脱敏必须在数据写入前完成,严禁存完再脱敏;数据库字段级脱敏应在dto层实现,dao层只处理原始数据;http响应脱敏需通过json.marshaler接口实现,避免中间件误操作;配置加载阶段须用mapstructure.decodehookfunc实时脱敏;内存中敏感字符串必须用runtime/secret.do隔离并擦除。

敏感数据不能“存完再脱敏”,脱敏必须发生在写入前——否则明文已在数据库、日志、缓存中残留,补救毫无意义。
数据库字段级脱敏必须在 DTO 层完成
DAO 层只负责查/写原始数据,脱敏逻辑绝不能塞进 SQL 或 ORM 的 Scan/Value 方法。一旦这么做,缓存命中时绕过 DAO 就漏脱敏;单元测试 mock DAO 时也测不到脱敏行为。
- 统一收口在 HTTP handler 或响应 DTO 构建阶段:比如 Gin 中构造
userResp := UserResponse{Phone: maskPhone(u.Phone)} - 避免在结构体字段上直接赋值脱敏后字符串——这会污染领域模型;DTO 应是独立类型,与 domain struct 分离
- 若需复用,可为常用字段定义脱敏函数:
maskPhone、maskEmail、maskIDCard,参数和返回值都用string,不带 context 或配置
JSON 响应脱敏只能靠 json.Marshaler 接口
别用中间件读 c.Writer.Body 或对 json.Marshal 返回的 []byte 做 strings.ReplaceAll——HTTP 响应体可能已 flush,且正则会误杀 URL、错误码、嵌套 JSON 的引号和转义符。
- 实现指针接收器方法:
func (u *User) MarshalJSON() ([]byte, error),否则无法处理u.Profile.Phone这类嵌套字段或 nil 指针 - 内部必须用别名类型防递归:
type Alias User,再以(*Alias)(u)转换 - 脱敏值不能为空字符串:如果字段带
json:",omitempty",空串会让字段消失;改用"*** **** ***"或"[REDACTED]" - 权限开关靠闭包捕获,例如:
redactEnabled := isRedactForRole(ctx.Value("role")),不能往MarshalJSON签名里塞context.Context
配置文件加载阶段就要脱敏
viper.Unmarshal 完再遍历结构体字段“打码”,已经晚了——密码等字段早已明文驻留内存,可能被 pprof 或 crash dump 泄露。
- 必须用
mapstructure.DecodeHookFunc,在反序列化过程中拦截字符串字段 - 判断依据是 key 路径(如
"database.password"),不是字段值内容;用strings.Contains(strings.ToLower(keyPath), "password")等方式匹配 - 目标结构体字段必须导出(首字母大写),否则
mapstructure不会设置它;小写字段 + hook = 脱敏失效 - 环境变量覆盖配置时,
viper.BindEnv必须显式调用,并在绑定前手动擦除值:viper.BindEnv("db.password", "DB_PASSWORD"); os.Setenv("DB_PASSWORD", "[REDACTED]")
内存中敏感字符串必须用 runtime/secret.Do 隔离
Go 的 string 不可变,pwd = "" 或 strings.ReplaceAll(pwd, ...) 都不擦除底层字节——它们可能还在栈、堆或只读段里。
- 所有涉及密钥、令牌、密码的操作,必须包裹在
runtime/secret.Do中 - 先转成临时
[]byte:pwdBytes := []byte(pwd),别直接对 string 字面量操作 - 函数退出后,
pwdBytes所在栈帧和寄存器立即清零;关联堆内存由 GC 在下次清扫时擦除 - 返回结果只能是非敏感派生值(如 hash、token ID),不能返回原切片或其子切片
真正麻烦的从来不是怎么写脱敏逻辑,而是确保每条数据路径——配置加载、数据库读写、HTTP 响应、日志输出、panic 堆栈——都各自守住自己的脱敏边界。漏掉任意一环,前面所有努力就归零。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











