真正可控的脱敏点只有两个:实现json.Marshaler接口或包装http.ResponseWriter;中间件无法劫持c.JSON()的json.Marshal()过程,因gin.Writer默认不缓存响应体,字节直推TCP。

直接在 c.JSON() 里做脱敏是无效的——它只是调用 json.Marshal(),而中间件无法劫持这个过程;真正可控的脱敏点只有两个:json.Marshaler 接口实现,或包装 http.ResponseWriter 替换写入逻辑。
为什么中间件里读不到原始 JSON 字节
gin 的 c.Writer 默认不缓存响应体。一旦 c.JSON() 调用 WriteHeader() 或首次 Write(),字节就直推 TCP 连接,后续中间件根本拿不到原始 JSON 内容。
- 错误做法:
c.Next()后试图读c.Writer.Body—— gin 根本不暴露该字段 - 正确做法:在
c.Next()前用自定义ResponseWriter替换c.Writer,重写Write([]byte)和WriteHeader(int) - 仅对
Content-Type: application/json做处理,其他类型(如text/html、application/octet-stream)必须直通,否则会破坏静态资源或文件下载
用 json.Marshaler 实现字段级脱敏
这是最稳定、零 GC 开销、类型安全的方式。它不改原始数据,只影响最终 JSON 输出,且能响应上下文(如权限、环境)。
使用 JSON Schema 验证 JSON 数据,从示例 JSON 生成 schema,并将其转换为 TypeScript 接口、Python 数据类或 Markdown 文档。
- 必须用指针接收器:
func (u *User) MarshalJSON() ([]byte, error),值接收器会导致嵌套结构体或nil指针 panic - 防递归关键:定义别名类型
type userAlias User,再用(*userAlias)(u)转换后调用json.Marshal - 脱敏值禁用空字符串:若字段带
json:",omitempty",脱敏成""就会整个字段消失;建议用"[REDACTED]"或固定掩码如"*** **** ***" - 权限判断不能塞进方法签名(
json.Marshal不传context.Context),得靠闭包捕获,比如redactEnabled := isRedactEnabled(ctx)后在闭包内使用
struct tag 控制脱敏开关与规则
不能靠中间件自动识别“手机号”“身份证”,必须显式声明。推荐用自定义 tag 如 secure:"phone,mask:4",配合生成器或反射解析。
- 常见错误:在中间件里对所有
string字段统一替换为"***"→ 昵称、城市名、商品标题全被误杀 - 可行路径只有两条:
secure:"phone"这类 struct tag 标记,或统一响应结构(如Response{Data: interface{}},只处理Data字段) - 若用反射扫描,必须限定白名单类型(如
*User、[]Order),无法安全处理interface{}或匿名嵌套结构体 - 高性能场景建议用
go:generate为常用结构体生成静态Redact(ctx context.Context) *User方法,规避运行时反射开销
日志与响应脱敏必须分开处理
敏感信息不能靠“不打印”来隐藏。HTTP 响应、日志、gRPC、调试输出,每个出口都要独立控制脱敏逻辑,混用一套中间件必然漏脱。
- 请求体脱敏:应在
c.ShouldBindJSON()前完成,用json.Unmarshal到map[string]interface{}后递归检查 key 是否在预设敏感键集合中(如["password", "token", "idCard"]),只替换字符串类型 value - query 参数脱敏:用
r.URL.Query()获取后遍历,但注意同步更新r.URL.RawQuery,否则日志看到的是未脱敏原始串 - 别用字符串替换、正则扫 JSON 字节流、gzip 解压再改内容——上线即翻车:要么漏字段,要么破格式,要么 panic
最易被忽略的点是:脱敏逻辑必须和序列化入口强绑定,而不是和 HTTP 生命周期绑定。同一个 User 结构体,在 c.JSON()、slog.Info()、数据库写入、甚至 fmt.Printf() 中的行为必须各自可控——靠中间件统一拦截,本质是在赌字段名不会变、不会嵌套、不会动态生成。

















