gin中敏感字段屏蔽不能依赖中间件字符串扫描,必须采用结构体显式标记+marshaljson接口控制或responsewriter包装替换;前者需指针接收器、类型别名防递归、闭包捕获权限、脱敏值用"[redacted]"避免omitempty丢失,后者须前置替换writer并仅处理json类型。

Gin 框架里做敏感字段屏蔽,不能靠中间件“扫响应体字符串”来实现——这种做法在真实服务中基本等于埋雷。真正可控、可维护的方式只有两种:结构体层显式标记 + 序列化入口拦截,或 ResponseWriter 层包装替换。
用 MarshalJSON 接口控制输出字段
这是最稳定、最易测试的路径。Gin 默认走 json.Marshal,而它会自动调用实现了 MarshalJSON() 方法的类型。关键点不是“加个中间件”,而是让敏感字段自己决定怎么序列化。
- 必须用指针接收器:
func (u *User) MarshalJSON() ([]byte, error),否则嵌套结构(如u.Profile.Phone)无法触发脱敏 - 防递归:定义别名类型
type Alias User,再用(*Alias)(u)调用原生序列化,避免无限循环 - 权限判断需闭包捕获:比如
redactEnabled := isRedactEnabled(c.Request.Context()),然后在MarshalJSON里用这个布尔值控制掩码逻辑 - 脱敏值别用空字符串:
json:",omitempty"字段脱敏后为空就会消失;建议统一返回"[REDACTED]"或固定格式如"*** **** ***"
用自定义 ResponseWriter 包装 JSON 响应
当无法修改结构体(比如第三方库返回的 struct),或需要统一处理所有 application/json 响应时,必须替换 c.Writer。Gin 的 c.Writer 是接口,可以被包装,但不能等 c.Next() 完了再去读——此时响应早已 flush 到 TCP 连接。
- 替换时机:在
c.Next()前完成c.Writer = &bodyLogWriter{...},重写Write()和WriteHeader() - 只处理
Content-Type: application/json:其他类型(text/html、image/png)直通,避免破坏静态资源 - 解析 JSON 别全量
Unmarshal:用json.Decoder.Token()流式匹配 key 路径(如user.phone),减少内存和 GC 压力 - 不要试图正则扫字节流:
"phone":"[^"]*"这种写法在换行、空格、转义符不一致时必然漏字段或 panic
sub_filter 在 Nginx 层做兜底?小心失效场景
Nginx 的 sub_filter 只能作为最后防线,且高度依赖后端输出格式。它不是“智能脱敏”,只是纯文本查找替换。
- 必须确认目标字符串以**完全一致的明文形式**出现在未压缩响应体中:用
curl -H "Accept-Encoding: identity"抓包验证 -
sub_filter_types必须显式包含application/json,默认只处理text/html -
sub_filter_once off是必须项:JSON 常有多个同名字段(如用户列表里的每个"phone"),只替换第一个毫无意义 - gzip 响应必须配
gunzip on,否则sub_filter看不到原始内容;但开启 gunzip 会增加 CPU 开销 - 永远别指望它处理动态拼接字段:JS 渲染的
data-phone、模板里插的{{.Token}}、环境变量注入的"api_key": "${API_KEY}"—— 全都不生效
最容易被忽略的是:字段级权限和脱敏是两件事。一个字段是否可见,取决于当前请求上下文(context.Context 中的用户角色、租户 ID、是否本人),而不是结构体定义本身。把权限判断硬编码进 MarshalJSON 或塞进中间件里做字符串替换,都会导致逻辑耦合、难以测试、上线后漏字段。











