脱敏必须发生在数据离开服务层之后、渲染到前端之前,否则字段可能在缓存、日志、rpc序列化等环节裸奔;dao层脱敏是反模式,http响应需通过json.marshaler接口统一收口,用指针接收器、别名类型防递归、闭包捕获权限、非空掩码值,并结合struct tag与key路径精准控制。

脱敏必须发生在数据离开服务层之后、渲染到前端之前,否则字段可能在缓存、日志、RPC序列化等环节裸奔。DAO 层做脱敏是典型反模式——它破坏缓存一致性、让测试无法绕过逻辑、且极易漏调用。
HTTP 响应脱敏:用 json.Marshaler 接口统一收口
所有 HTTP 接口最终都走 json.Marshal,这是唯一可控、类型安全的脱敏入口。别在 handler 里手动调 maskPhone(u.Phone),容易漏字段、难维护。
- 必须用指针接收器:
func (u *User) MarshalJSON() ([]byte, error),否则嵌套结构体(如u.Profile.Address)和 nil 指针无法处理 - 防递归关键:定义别名类型
type UserAlias User,再用(*UserAlias)(u)调用原生 marshal,避免无限循环 - 脱敏值不能为空字符串:若字段带
json:",omitempty",脱敏后变空会导致字段消失;建议固定掩码如"*** **** ***" - 权限判断靠闭包捕获:
redactEnabled := isRedactEnabled(ctx),不能塞进方法签名(json.Marshal不传context.Context)
配置文件敏感字段:在 mapstructure.DecodeHookFunc 中按 key 路径过滤
环境变量或 YAML 配置里的 database.password、api.secret_key 等字段,必须在反序列化时就脱敏,而不是等结构体构建完再扫字段值。
- hook 函数需严格比对完整 key 路径,例如
"database.password"或"auth.jwt.private_key",不能只看字段值是否含 "pass" -
viper.Unmarshal目标结构体字段必须首字母大写(导出),且传入指针;环境变量覆盖时要显式viper.BindEnv("DATABASE_PASSWORD", "DB_PASSWORD")并手动脱敏 - 避免用
strings.Contains(val, "key")这类模糊匹配——URL 中的https://api.example.com/v1?key=xxx会被误杀
日志与 panic 输出:用接口接管而非字符串替换
fmt.Println 和 log.Printf 默认会打印 unexported 字段,slog 更会绕过 fmt.Formatter 直接取值。靠正则替换日志内容或中间件拦截 body 是高危操作。
- 防
fmt打印:实现fmt.Formatter,返回""即可覆盖所有%v/%s场景 - 防
slog泄露:额外实现slog.Valuer接口,确保logger.Info("login", "token", t)日志中token字段被安全遮蔽 - panic 信息不走
String()方法:要用*SafeError封装,recover时统一调err.Error(),否则堆栈里直接暴露原始 token
最常被忽略的一点:HTTP 响应脱敏了,不代表配置文件、panic 日志、审计日志、gRPC 返回、甚至 pprof 内存 dump 里的敏感字段也自动安全。每个出口必须独立收口,且不能依赖字段值内容做判断——必须结合 key 路径 + 类型反射 + struct tag(如 secure:"phone")联合判定。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











