脱敏必须在日志构造阶段完成,而非依赖Hook事后处理;应通过实现MarshalLogObject、MarshalJSON接口或封装SanitizeMap等方案,在数据源头精准控制敏感字段输出。

zap 的 Hook 无法安全脱敏日志内容
Hook 层面对 entry.Message 或 entry.Fields 做字符串替换,本质是“事后补救”,会破坏结构化日志语义。比如 zap.String("password", "123456") 在 Hook 里被替换成 "***" 后,字段名 password 和值都混在字符串里,无法区分;更危险的是,如果日志里有 URL:"https://api.com/login?password=abc",正则一扫就把整个 query 干掉,或误杀 password_reset_token 这类合法字段名。
常见错误现象:
- 日志 JSON 格式损坏(引号错位、key/value 错配)
- 嵌套结构体里的敏感值漏脱敏(Hook 只看到扁平化后的字符串)
- 同一字段名在不同层级重复出现,替换逻辑失控
logrus 的 Hook 脱敏同样不可靠
logrus.Hook 接口的 Fire 方法接收的是 logrus.Entry,其 Data 是 map[string]interface{},但 Hook 内部无法控制该 map 的序列化时机——它只负责“记录前最后一步”,而 logrus 默认把 Data 直接转成字符串再拼进日志行。你改 entry.Data 里的值,若字段是 struct 指针或 interface{},仍可能逃逸脱敏逻辑。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 不要在 Hook 里对
entry.Data做递归遍历修改——logrus不保证Data是深拷贝,改了可能污染原始业务对象 - 避免用
fmt.Sprintf拼接日志后再塞回entry.Message,这等于放弃结构化能力 - 若坚持用 logrus,唯一安全路径是让业务结构体实现
fmt.Stringer,在String()方法里做字段级脱敏
真正生效的脱敏必须发生在日志构造阶段
所有主流日志库(zap、zerolog、logrus)都不提供“写入前自动识别敏感字段”能力。它们只负责序列化你传进去的东西。所以脱敏动作必须提前到调用日志方法之前,且要绑定到具体数据载体上。
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
可行方案:
- 对结构体实现
zapcore.ObjectMarshaler接口(zap场景),在MarshalLogObject中显式控制每个字段输出值 - 对结构体实现
json.Marshaler接口(通用场景),在MarshalJSON中只对已知敏感 key 替换 value,保持类型和结构不变 - 封装一个
SanitizeMap工具函数,对map[string]interface{}递归处理,但仅替换string类型的 value,且只匹配白名单 key(如"password"、"api_key"、"id_card")
注意:脱敏值不能设为空字符串,否则带 json:",omitempty" tag 的字段会直接消失;推荐统一用 "[REDACTED]" 或固定掩码如 "***"。
HTTP 请求日志脱敏需单独处理 Header 和 Body
框架中间件(如 gin)的日志钩子拿到的是 *gin.Context,但 c.Request 的 Body 是单次读取流,Header 是独立 map。试图在中间件里用 Hook 统一脱敏,既拿不到原始 body 字节,也容易漏掉 Authorization、Cookie 等高危 header。
正确做法:
- 在中间件开头用
io.ReadAll(c.Request.Body)读取并重置c.Request.Body,同时缓存副本用于日志 - 显式提取
c.Request.Header.Get("Authorization"),按前缀判断后脱敏("Bearer ***"、"Basic ***") - 用
http.ParseCookie解析Cookie字段,逐个检查 key 是否含"token"、"session"等关键词,只替换对应 value - 对 query 参数用
c.Request.URL.Query()遍历,但记得同步更新c.Request.URL.RawQuery,否则日志里显示的仍是未脱敏版本
脱敏不是加个 Hook 就完事的事——它必须和数据源头强绑定,否则要么漏,要么崩,要么误杀。最易被忽略的点是:Header 和 Body 的脱敏逻辑完全独立于日志库 Hook,必须在中间件里显式编写,且顺序不能错(先读 body,再脱敏,再放行)。

















