接口返回数据脱敏必须在json.Marshal环节通过json.Marshaler接口实现,禁用中间件字符串替换;需用指针接收器、别名类型防递归、闭包捕获权限开关、非空占位符替代omitempty冲突,并按场景分层处理日志、HTTP、RPC脱敏。

接口返回数据脱敏不能靠中间件全局替换字符串,必须在序列化出口处控制——也就是 json.Marshal 这一环。否则字段漏脱、格式错乱、nil panic 都是线上事故常态。
用 json.Marshaler 接口做可控脱敏
这是最稳的方式:结构体自己决定怎么转 JSON,原始字段值完全不动,日志、DB、RPC 内部仍可用明文,仅对外输出时变形。
- 必须用指针接收器:
func (u *User) MarshalJSON() ([]byte, error),否则嵌套结构或 nil 字段无法安全处理 - 防递归要定义别名类型:
type Alias User,再构造匿名 struct 包裹它,避免无限调用自身 - 脱敏开关建议从 context 或全局 config 读,别硬编码;比如
if redact.Enabled(ctx) { ... } - 别手动拼
"{" + ... + "}",用json.Marshal(aux)或json.NewEncoder,否则引号、转义、nilmap 全会崩
struct tag 声明脱敏规则比 runtime 反射更可靠
想让同一结构体在不同环境/角色下行为不同(如管理员看全量、访客只看脱敏),用 tag 是唯一可静态校验、IDE 可跳转、无 magic string 的方式。
- 推荐格式:
phone string `json:"phone" redact:"mask"`或id_card string `json:"id_card" secure:"true"` - 反射处理时必须检查:
v.Kind() == reflect.Ptr就先v.Elem(),v.Kind() == reflect.Interface就v.Interface()后再判断类型 - 私有字段(
CanInterface() == false)直接跳过,别强求;业务敏感字段本就该是 public - 高频接口别实时反射——性能掉 10 倍以上;可用
go:generate为常用结构体生成Redact()方法,零运行时开销
omitempty 和脱敏值冲突怎么办
如果脱敏后字段变成空字符串(""),又带 omitempty,那整个字段就从 JSON 里消失了,前端可能报 undefined 错误。
立即学习“go语言免费学习笔记(深入)”;
- 要么去掉
omitempty,确保字段始终存在 - 要么脱敏值设为占位符,比如
"[REDACTED]"或"***",保持非空 - 特别注意时间字段、数字字段:脱敏后不能返回
"",否则json.Unmarshal会失败
日志、HTTP、RPC 脱敏不能共用一套逻辑
日志需要字段级过滤(如 Password 永远打成 "******"),HTTP 返回需按角色动态脱敏,RPC 传输可能要求端到端加密——它们的触发时机、上下文、性能约束完全不同。
- 日志脱敏优先走
Sensitive接口(如 go-zero 的MaskSensitive()),框架自动识别并调用 - HTTP handler 里别实时反射脱敏,应提前调用结构体自己的
MarshalJSON或 DTO 转换 - RPC 场景若用 Protobuf,直接用
redact.v3.value注解(如string email = 3 [(redact.v3.value).string = "r*d@ct*d"];),由插件生成脱敏代码 - 所有脱敏逻辑必须保留原始字段类型(string → string,int64 → int64),否则下游反序列化失败
真正难的不是写脱敏函数,而是统一脱敏边界:哪些层该脱、哪些不该脱、谁来决定开关、错误时是否降级。这些决策一旦分散在各 handler 里,不出三个月就没人说得清哪条数据到底有没有被脱过。


















