脱敏逻辑应放在http handler或dto转换层,而非dao层;推荐在构建响应结构体时统一调用maskphone等函数,或通过自定义marshaljson实现自动脱敏,确保所有出口路径受控且幂等。

脱敏逻辑该放在哪一层?别塞进 DAO
数据库敏感字段(比如手机号、身份证号)的脱敏展示,核心原则是:脱敏必须发生在数据离开服务层之后、渲染到前端之前。把 maskPhone 这类函数写在 DAO 层或 SQL 查询里,会导致缓存失效、测试困难、复用性差——更糟的是,一旦某处漏调用,敏感数据就直接裸奔。
推荐做法是统一收口在 HTTP handler 或 DTO 转换层。例如 Gin 框架中,在构造响应结构体时做脱敏:
type UserResp struct {
ID int `json:"id"`
Name string `json:"name"`
Phone string `json:"phone"`
IDCard string `json:"id_card"`
}
func buildUserResp(u *UserModel) *UserResp {
return &UserResp{
ID: u.ID,
Name: u.Name,
Phone: maskPhone(u.Phone), // 调用脱敏函数
IDCard: maskIDCard(u.IDCard), // 同上
}
}
这样既保证所有出口路径都经过同一套规则,也方便后续替换脱敏策略(比如改用 AES 加密前缀+固定掩码)。
maskPhone 该怎么写才不踩正则坑
用正则替换手机号最常见错误是硬编码 ^1[3-9]\d{9}$ 去匹配再替换——这会漏掉带区号、空格、括号的输入(如 "138 1234 5678"),也会误伤非手机号字段(比如订单号以 138 开头)。真正健壮的做法是:只对明确标记为「手机号」的字段做格式化脱敏,且保留原始格式特征。
建议用固定位置掩码 + 原始字符串清洗:
- 先用
strings.ReplaceAll清除空格、短横线、括号等干扰字符 - 再判断清洗后是否为 11 位纯数字;不是则原样返回(避免破坏非手机号内容)
- 是则取前 3 位 +
"****"+ 后 4 位,保持可读性与合规性
示例函数:
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
func maskPhone(s string) string {
cleaned := regexp.MustCompile(`[\s\-\(\)\+]`).ReplaceAllString(s, "")
if len(cleaned) != 11 || !regexp.MustCompile(`^\d{11}$`).MatchString(cleaned) {
return s // 不符合手机号特征,不脱敏
}
return cleaned[:3] + "****" + cleaned[7:]
}
Gin 中如何让脱敏自动生效,避免每个 handler 都手动调用
手动调用 buildUserResp 容易遗漏。Gin 自身不提供字段级序列化钩子,但可以用自定义 JSON marshaler 实现透明脱敏。
关键点:给响应结构体字段加 tag 标记,例如 json:"phone" mask:"phone",然后重写 MarshalJSON 方法:
type UserResp struct {
ID int `json:"id"`
Name string `json:"name"`
Phone string `json:"phone" mask:"phone"`
IDCard string `json:"id_card" mask:"idcard"`
}
func (u *UserResp) MarshalJSON() ([]byte, error) {
type Alias UserResp // 防止递归调用
raw, _ := json.Marshal(&struct {
*Alias
Phone string `json:"phone"`
IDCard string `json:"id_card"`
}{
Alias: (*Alias)(u),
Phone: maskPhone(u.Phone),
IDCard: maskIDCard(u.IDCard),
})
return raw, nil
}
这样只要返回 *UserResp,Gin 就会自动触发脱敏。注意:该方法仅适用于明确控制的响应结构体,不要在基础模型(如 UserModel)上实现,否则会影响 DB 层操作。
数据库查询时要不要提前脱敏?多数情况不该
有人想在 SQL 层用 CONCAT(LEFT(phone,3), '****', RIGHT(phone,4)) 直接查出脱敏结果——这看似省事,实则埋雷:
- 脱敏后字段无法用于搜索(比如模糊查手机号)、校验、导出原始数据
- 不同接口对脱敏粒度要求不同(管理后台可能需显示中间 4 位,APP 只显示前 3 后 4)
- ORM 如 GORM 的预加载(
Preload)会因字段被改写而失效
唯一适合 SQL 脱敏的场景是:日志表或审计表这类只读、无业务逻辑的辅助表。正常业务表请坚持「存储明文、展示脱敏」原则,靠应用层统一控制出口。
真正容易被忽略的点是:脱敏函数必须幂等且无副作用。比如 maskPhone 不能修改入参字符串,也不能依赖全局状态或外部配置(除非配置是只读常量)。否则在并发请求下可能引发不可预期行为。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










