结构体字段脱敏必须实现marshallogobject接口,导出敏感字段并显式调用enc.addstring("password", "[redacted]");zerolog需用interfacehook+sanitizemap深度递归处理map[string]interface{};http请求需单独脱敏query、header及调试模式下浅析body。

zap日志里结构体字段脱敏必须实现MarshalLogObject
直接把User结构体传给zap.Any("user", u),敏感字段如Password、Token会原样打印——zap不识别字段语义,也不扫描字段名。唯一可控路径是让结构体实现zapcore.ObjectMarshaler接口(即MarshalLogObject方法)。
常见错误包括:
• 字段未导出(如password string),反射无法访问
• 方法内未判空就解引用u.Credentials.Token,导致日志panic
• 脱敏后返回空字符串,遇上json:",omitempty"字段直接消失
正确做法:
• 用导出字段:Password string
• 在MarshalLogObject中只调用enc.AddString("password", "[redacted]")等显式方法
• 避免递归遍历嵌套结构体,深度超过两层时优先手动展开关键字段
zerolog控制台脱敏得靠InterfaceHook + SanitizeMap
zerolog没有MarshalLogObject机制,InterfaceHook是唯一能拦截日志参数的入口,但它只接收interface{},对map[string]interface{}最友好。
典型误用:
• 全局正则替换所有含"password"的字符串,把user_password_reset_url也干掉了
• 没递归处理嵌套map,漏掉req.Header["Authorization"]
• 在hook里尝试修改int或time.Time类型,引发panic
推荐封装SanitizeMap(m map[string]interface{}):
• 只匹配键名("password"、"api_key"、"secret"等),不碰值内容
• 深度递归时先判断v.Kind() == reflect.Map再进入下一层
• 非map类型直接返回原值
HTTP请求日志必须单独脱敏req.URL.Query()和req.Header
别指望一个InterfaceHook或MarshalLogObject覆盖全部。HTTP请求中敏感数据集中在三个位置:req.URL.Query()(如?token=xxx)、req.Header(如Authorization)、req.Body(如JSON中的password字段)。
其中req.Body最难处理——它是一次性流,读完就不可再读。所以实际做法是:
• 对Query()和Header做白名单键匹配("token"、"authorization"、"x-api-key")
• 对Body只在调试模式下浅层解析(限Content-Type: application/json且长度• 绝不尝试重写req.Body或用io.TeeReader拦截,Gin中间件里没地方存原始body
配置加载阶段就该脱敏,mapstructure.DecodeHookFunc是关键
viper.Unmarshal(&cfg)之后再遍历结构体“打码”已经晚了——database.password等字段早已明文驻留内存,可能被pprof或crash dump捕获。
真正安全的时机是反序列化过程中:
• 注册mapstructure.DecodeHookFunc,按key路径拦截(如strings.Contains(strings.ToLower(keyPath), "password"))
• hook函数只对reflect.String类型生效,其他类型直通
• 不要试图在hook里做复杂逻辑,只做简单字符串替换(如"***")或返回"[redacted]"
• keyPath获取需配合viper.AllKeys() + 正则预匹配,避免依赖调用栈(不稳定)
控制台日志脱敏最易被忽略的点:不是“怎么打码”,而是“在哪打码”。同一份敏感数据可能同时出现在结构体字段、HTTP头、URL参数、配置项和日志上下文中,每个出口都得有独立控制点。没有银弹,只有分层收口。











