因为json标签只在直接调用json.marshal时生效,对interface{}、指针nil、嵌套结构体及orm返回的map[string]interface{}无效,且子结构体标签不自动继承,无法保障动态数据脱敏安全。

敏感字段为什么不能只靠结构体标签过滤
因为 json 标签(如 json:"-" 或 json:"name,omitempty")只在直接调用 json.Marshal 时生效,而 Gin 的 c.JSON 内部确实用了它——但问题出在嵌套结构体、指针字段、map 或 interface{} 类型上。一旦某层是 interface{}(比如从数据库查出后未强转),json 标签就完全失效;同理,指针字段为 nil 时若没加 omitempty,仍可能输出 null,反而暴露字段存在性。
- 别依赖
json:"-"处理动态数据或 ORM 返回的map[string]interface{} - 嵌套结构体中,子结构体字段的
json标签不会被父级自动继承,必须每一层都显式声明 - Gin 的
c.ShouldBindJSON不会校验输出字段,只管入参解析;隐藏逻辑必须在响应前完成
用自定义 MarshalJSON 实现可控脱敏
对敏感结构体实现 MarshalJSON 方法,是最直接且类型安全的方式。它绕过默认反射逻辑,由你完全控制哪些字段序列化、如何处理空值或加密替换。
例如用户模型需隐藏 Password 和 IDCard:
func (u User) MarshalJSON() ([]byte, error) {
type Alias User // 防止无限递归
return json.Marshal(&struct {
*Alias
Password string `json:"password,omitempty"`
IDCard string `json:"id_card,omitempty"`
}{
Alias: (*Alias)(&u),
Password: "******",
IDCard: u.maskIDCard(),
})
}
- 必须用
type Alias User避免方法递归调用自身 - 字段名保持原结构体大小写,否则 JSON key 会变(如
Password→password) - 如果字段是指针(如
*string),需判空再赋值,否则解引用 panic - 该方法仅对值接收者有效;若用指针接收者,
c.JSON(200, &u)才触发
Gin 中间件统一拦截响应体做字段擦除
当项目已有大量结构体、又无法修改其定义时,中间件 + 字段路径匹配是更灵活的选择。核心是劫持 ResponseWriter,在写入前对 body 做 JSON Patch 或字段删除。
推荐用 github.com/tidwall/gjson 解析 + github.com/tidwall/sjson 修改,避免反序列化全量结构:
func SanitizeMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
rw := &responseWriter{ResponseWriter: c.Writer, body: &bytes.Buffer{}}
c.Writer = rw
c.Next()
if c.Writer.Status() == 200 && strings.Contains(c.Writer.Header().Get("Content-Type"), "application/json") {
data := rw.body.Bytes()
// 删除 user.password、user.profile.phone 等路径
cleaned, _ := sjson.DeleteBytes(data, "user.password")
cleaned, _ = sjson.DeleteBytes(cleaned, "user.profile.phone")
c.Writer.WriteHeader(c.Writer.Status())
c.Writer.Write(cleaned)
}
}
}
- 不要用
json.Unmarshal→ 修改 →json.Marshal,大响应体下 GC 压力高 -
sjson.DeleteBytes支持嵌套路径(如"data.items.#.token"),但不支持通配符模糊匹配 - 中间件必须在
c.Next()后操作,否则body还没写入 - 注意并发安全:每个请求应有独立
bytes.Buffer,别复用全局 buffer
数据库层返回前就做字段裁剪最省资源
真正高效的脱敏不是在 HTTP 层补救,而是让敏感字段根本不出现在 Go 对象里。ORM 查询时主动 omit 字段,比后续各种 Marshal/中间件都轻量。
- 使用 GORM 时,用
Select("id,name,email")明确指定字段,而非Find(&users) - SQL 查询直接写
SELECT id, name, email FROM users,避免* - 如果用
map[string]interface{}接收查询结果,用delete(m, "password")比后期 JSON 处理快一个数量级 - 注意:GORM 的
Scan方法若目标 struct 有未查询字段,会静默置零,但不会报错——容易误以为字段被脱敏了,其实只是没读进来
多层脱敏的关键不在“怎么藏”,而在“在哪藏”。越靠近数据源头处理,越少 runtime 开销,也越难被绕过。但也要小心:过度依赖某一层(比如只靠中间件)会导致测试难覆盖、调试难定位——特别是字段路径写错或嵌套层级变动时,静默失效比报错更危险。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











