在生产环境中,日志往往是排查问题的“救命稻草”,但也可能成为泄露隐私的“定时炸弹”。你是否曾在日志中无意间看到过用户的明文密码、银行卡号或手机号?一旦这些敏感信息随日志进入集中化存储系统,不仅面临合规风险,更可能造成不可估量的安全事故。
传统的做法是在每一个打印日志的地方手动处理脱敏,但这既繁琐又容易遗漏。Go 1.21 引入的 log/slog 提供了一个优雅的解法:通过自定义 Handler,可以将脱敏逻辑收拢到基础设施层,实现“一处配置,全局生效”。
这篇文章将手写一个专门用于隐私脱敏的自定义 Handler。
slog:日志安全的最后一道防线
在 slog 的架构中,Handler 是日志输出前的最后一站。无论业务代码中如何调用 logger.Info,最终都会流向 Handler 的 Handle 方法。这为我们提供了一个天然的拦截点。
首先,需要定义一个脱敏处理器的结构:
// RedactHandler 包装了原有的 Handler 并提供脱敏功能
type RedactHandler struct {
next slog.Handler
sensitive []string // 需要脱敏的键名列表
}
这里采用了装饰器模式。RedactHandler 内部持有一个 next Handler(如标准的 JSONHandler),并在其基础上增加脱敏逻辑。
实战:拦截并重构日志记录
实现脱敏的核心在于 Handle 方法。通过遍历日志记录中的所有属性,识别出敏感字段并进行替换。
// Handle 方法是拦截并处理属性的核心
func (h *RedactHandler) Handle(ctx context.Context, r slog.Record) error {
newRecord := slog.NewRecord(r.Time, r.Level, r.Message, r.PC)
r.Attrs(func(a slog.Attr) bool {
newRecord.AddAttrs(h.redactAttr(a)) // 调用递归脱敏逻辑
return true
})
return h.next.Handle(ctx, newRecord)
}
在上面的代码中,并未直接修改原始的 Record,而是创建了一个新的 Record 并将过滤后的属性逐一添加。这是因为 slog.Record 应该是不可变的,直接修改可能会影响到同一条日志的其他处理分支。
处理嵌套的分组属性
在复杂的业务日志中,属性往往不是扁平的,而是嵌套在“分组”里的(例如 user.password)。这篇文章实现的脱敏逻辑也需要能够深入嵌套结构。
// redactAttr 递归处理属性,包括分组中的嵌套属性
func (h *RedactHandler) redactAttr(a slog.Attr) slog.Attr {
if a.Value.Kind() == slog.KindGroup {
attrs := a.Value.Group()
newAttrs := make([]any, len(attrs))
for i, attr := range attrs {
newAttrs[i] = h.redactAttr(attr)
}
return slog.Group(a.Key, newAttrs...)
}
if h.isSensitive(a.Key) {
return slog.String(a.Key, "***")
}
return a
}
通过递归处理,可以确保无论敏感信息藏得有多深,都能被准确识别并戴上“面具”。需要注意的是,处理分组时要保持原有的层级关系,避免破坏日志的结构化语义。
细节:性能与并发安全的博弈
自定义 Handler 必须是并发安全的。除了 Handle 方法,还需要妥善处理 WithAttrs 和 WithGroup。
// WithAttrs 必须先脱敏再传递给下一个 Handler
func (h *RedactHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
if len(attrs) == 0 {
return h
}
redacted := make([]slog.Attr, len(attrs))
for i, a := range attrs {
redacted[i] = h.redactAttr(a)
}
newHandler := *h
newHandler.next = h.next.WithAttrs(redacted)
return &newHandler
}
// 别忘了实现 WithGroup 和 Enabled 接口
func (h *RedactHandler) WithGroup(name string) slog.Handler {
if name == "" {
return h
}
newHandler := *h
newHandler.next = h.next.WithGroup(name)
return &newHandler
}
func (h *RedactHandler) Enabled(ctx context.Context, lvl slog.Level) bool {
return h.next.Enabled(ctx, lvl)
}
这里的关键点是执行结构体的浅拷贝 newHandler := *h。如果直接在原结构体上进行修改,会导致不同协程之间的 Logger 状态互相污染,造成难以排查的 Bug。同时,Handler 接口要求实现完整的四个方法,千万别漏了 Enabled 和 WithGroup。
此外,为了极致的性能,应当尽量避免在 Handle 中进行大规模的切片分配。在生产环境下,可以考虑使用对象池(sync.Pool)来复用缓存,或者针对极高频的日志字段进行预处理。
总结:让隐私保护成为代码基因
通过自定义 slog.Handler 实现脱敏,不仅解决了“漏删”的问题,更将安全合规的理念深植于系统的底层架构中。
在实际项目中,开发者还可以基于此扩展出更复杂的逻辑,比如根据用户权限动态脱敏,或者将脱敏后的数据进行哈希处理以便在不暴露明文的情况下进行关联搜索。slog 的灵活性给了无限的可能,而保护用户隐私,正是开发者应尽的责任所在。