结构体字段脱敏必须实现marshallogobject或json.marshaler接口,使用导出字段、避免panic、统一用"[redacted]"替代空字符串,并针对日志、配置加载、http响应等各出口分别控制。

zap日志中结构体字段脱敏必须实现MarshalLogObject
直接传结构体给zap.Any或zap.Object,敏感字段(如Password、Token)会原样输出——zap不识别语义,也不扫描字段名。唯一可控路径是让结构体实现zapcore.ObjectMarshaler接口。
常见错误:在MarshalLogObject里调用未判空的u.Credentials.Token,导致日志panic;或字段首字母小写(password string),反射无法访问。
- 必须用导出字段:
Password string,而非password string - 避免在方法内做可能 panic 的操作(如未检查 nil 的指针解引用)
- 脱敏值建议用
"[redacted]",不用空字符串,防止json:",omitempty"导致字段消失 - 若结构体嵌套深,优先用
enc.AddString("token", "[redacted]")显式控制,别依赖递归遍历
zerolog里没有MarshalLogObject,得靠InterfaceHook或SanitizeMap
zerolog不提供结构体级钩子,InterfaceHook是唯一能拦截日志参数的入口,但它只接收interface{},对map[string]interface{}最友好。
典型误用:把InterfaceHook写成全局正则替换字符串,结果把user_password_reset_url里的password也干掉了;或没递归处理嵌套map,漏掉req.Header["Authorization"]。
- 推荐封装
SanitizeMap(m map[string]interface{}),只匹配键名("password"、"api_key"、"secret"等),不碰值内容 - 对
map做深度递归时,需判断v := reflect.ValueOf(val); v.Kind() == reflect.Map再进入下一层 - 别在hook里尝试修改非
map类型(如int、time.Time),直接返回原值 - HTTP请求日志建议单独脱敏
req.URL.Query()和req.Header,别指望一个hook覆盖全部
配置加载阶段就要脱敏,mapstructure.DecodeHookFunc是关键
viper.Unmarshal后补救已经晚了——敏感字段(如database.password)早已明文驻留内存。真正安全的时机是反序列化过程中,用mapstructure.DecodeHookFunc按key路径拦截。
容易踩坑:hook函数写了,但viper绑定的是小写字段password string,mapstructure跳过未导出字段;或环境变量VIPER_DATABASE_PASSWORD绕过hook直写结构体。
- 结构体字段必须导出:
Password string,且viper需用指针接收:viper.Unmarshal(&cfg) - hook中通过调用栈或上下文提取完整key路径(如
"database.password"),不能只看"password"子串 - 只处理
f.Kind() == reflect.String && t.Kind() == reflect.String,其他类型原样返回 - 环境变量需显式绑定:
viper.BindEnv("database.password", "DATABASE_PASSWORD"),并在os.Getenv后手动脱敏
HTTP响应脱敏不能靠中间件扫body,必须控制json.Marshal入口
gin或http中间件拿到ResponseWriter时,body通常已写出或不可读。试图io.ReadAll(c.Writer.Body)失败率极高,且gzip、chunked编码会让解析雪上加霜。
最稳方案是让业务结构体实现json.Marshaler,脱敏逻辑只发生在序列化那一刻,原始数据不动,也不依赖中间件顺序。
- 必须用指针接收器:
func (u *User) MarshalJSON() ([]byte, error),否则无法处理nil指针或嵌套字段 - 防无限递归:定义
type Alias User,用(*Alias)(u)转换后再marshal - 脱敏开关从闭包捕获(如
redactEnabled := isRedactEnabled(ctx)),因MarshalJSON不接受context.Context - 禁止返回空字符串——若字段带
json:",omitempty",脱敏后为空会导致字段丢失,统一用"[REDACTED]"
所有出口(日志、HTTP、gRPC、panic堆栈)的脱敏逻辑必须各自收口,不存在一个通用hook能覆盖全部。最易被忽略的是:配置加载时的内存驻留、HTTP响应的流式写出、以及结构体嵌套层级带来的反射边界问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











