脱敏必须在日志写入前完成,Go标准库及zap、zerolog等均不支持写入前自动脱敏;Hook或Writer层字符串替换会破坏结构化语义且无法区分字段名与值;唯一可行方案是在构造日志上下文时通过结构体标签+反射实现字段级脱敏。

脱敏规则必须在日志写入前生效,不能靠日志库后置过滤
Go 标准库 log 和主流第三方库(如 zap、zerolog)都不提供“写入前自动脱敏”机制。试图在 Hook 或 Writer 层做字符串替换,会丢失结构化字段语义,且无法区分字段名和字段值——比如把 "password" 当成普通文本误删。真正可行的路径只有一条:在构造日志上下文或调用日志方法时,就完成敏感字段的识别与替换。
用结构体标签 + 反射实现字段级脱敏(zap 场景)
对需要打日志的业务结构体,用自定义 struct tag 标记敏感字段,再封装一个脱敏函数,在传给 zap.Object 前处理。这种方式不侵入日志调用点,也不依赖日志库内部机制。
示例:
type User struct {
Name string `json:"name"`
Password string `json:"password" log:"redact"`
Token string `json:"token" log:"redact"`
Email string `json:"email"`
}
func Redact(v interface{}) interface{} {
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Ptr {
rv = rv.Elem()
}
if rv.Kind() != reflect.Struct {
return v
}
rt := reflect.TypeOf(v).Elem()
out := reflect.New(rv.Type()).Elem()
for i := 0; i < rv.NumField(); i++ {
field := rt.Field(i)
if tag := field.Tag.Get("log"); tag == "redact" {
out.Field(i).SetString("[REDACTED]")
} else {
out.Field(i).Set(rv.Field(i))
}
}
return out.Interface()
}
使用时:
user := User{Password: "123456", Token: "abc", Email: "a@b.c"}
logger.Info("user login", zap.Any("user", Redact(user)))
注意点:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 只支持导出字段(首字母大写),未导出字段会被跳过
- 嵌套结构体需递归处理,上述示例未展开,实际项目中建议用
gopkg.in/yaml.v3或mapstructure类库辅助 -
Redact返回新对象,原结构体不变,避免副作用
避免在 fmt.Sprintf 或字符串拼接中脱敏
常见错误是先拼好日志字符串,再用正则替换关键词:
msg := fmt.Sprintf("login failed for %s, pwd=%s", user.Name, user.Password)
// ❌ 错误:无法区分 pwd=xxx 中的等号右侧是否真为密码值
msg = regexp.MustCompile(`pwd=\S+`).ReplaceAllString(msg, "pwd=[REDACTED]")
logger.Warn(msg)
这种做法问题明显:
- 正则易误杀(如 URL 中的
pwd=xxx被当成密码) - 无法处理 JSON 字符串中的敏感字段(如
{"password":"123"}) - 破坏结构化日志能力,
zap.String("msg", msg)只剩一坨字符串 - 性能差,每次都要编译正则、扫描全文本
配置驱动的脱敏策略要绑定到具体日志调用点
全局开关(如环境变量 LOG_REDACT=true)只能控制开/关,不能指定哪些字段在哪些场景下脱敏。更实用的方式是让日志调用者显式声明脱敏意图:
// 定义可配置的脱敏策略类型
type LogField struct {
Key string
Value interface{}
Redact bool // 显式标记该字段是否脱敏
}
logger.Info("user created",
zap.String("event", "user_created"),
zap.Any("user", LogField{Key: "user", Value: user, Redact: true}),
)
然后在自定义 zap.Core 或中间层解析 LogField 并替换值。这样既能按需启用,又不会污染结构体定义,也方便单元测试模拟不同脱敏组合。
真正麻烦的不是写脱敏逻辑,而是厘清“谁决定脱敏”“在哪一层脱敏”“脱敏后是否还保留字段名”。这三个问题没想清楚,代码越写越绕,最后要么漏脱敏,要么脱过头。

















