敏感字段必须显式标记,不可依赖中间件自动识别;可行方案为用struct tag声明并反射扫描白名单类型,或实现json.marshaler接口;脱敏需在响应写出前通过自定义responsewriter流式处理json,避免破坏格式与性能。

敏感字段必须显式标记,不能靠中间件自动识别
中间件拿不到结构体字段语义,c.Request 和 c.Writer 都是字节流或原始 HTTP 对象,无法知道 User.Phone 是手机号、Order.IDCard 是身份证号。常见错误是写个中间件对所有 string 字段统一替换成 "***",结果昵称、城市名、商品标题全被误杀。
可行方案只有两种:
- 用
sensitive:"phone"这类 struct tag 显式声明,再配合反射扫描白名单类型(如*User、[]Order) - 统一响应结构体,比如
Response{Data: interface{}},只对Data字段做脱敏,避开Error、Code等元信息 - 不处理
interface{}或匿名嵌套结构体——反射无法安全推导字段路径,容易 panic
JSON 响应脱敏必须替换 ResponseWriter,不能等 c.Next() 后读 body
c.Writer 一旦调用 WriteHeader 或首次 Write,数据就直接发到 TCP 连接,中间件里再也拿不到原始 JSON。别试 c.Writer.Body——gin 根本不暴露这个字段;就算有,也早已 flush。
正确做法是在 handler 执行前,用自定义 ResponseWriter 替换 c.Writer,重写 Write([]byte) 和 WriteHeader(int):
- 只对
Content-Type: application/json处理,text/html、application/octet-stream直通,避免破坏静态资源或文件下载 - 推荐用
json.Decoder.Token()流式匹配 key 路径(如user.phone),而非全量json.Unmarshal,减少内存与 GC 压力 - 若响应体已 gzip 压缩,先解压再处理——但要注意,gzip 解压失败会导致整个响应中断,得加 recover
真正可控的脱敏入口是 json.Marshaler 接口
所有“正则扫 JSON 字节流”“字符串替换”“gzip 解压再改内容”的做法,在真实服务中要么漏字段、要么破格式、要么 panic。上线即翻车。
必须实现指针接收器:func (u *User) MarshalJSON() ([]byte, error),否则无法处理嵌套字段(如 u.Profile.Phone)或 nil 指针:
- 防递归关键:定义别名类型
type Alias User,再用(*Alias)(u)转换,避免MarshalJSON无限调用自身 - 权限判断不能塞进方法签名(
json.Marshal不传context.Context),得靠闭包捕获,比如redactEnabled := isRedactEnabled(ctx)后在闭包内使用 - 脱敏值禁用空字符串:若字段带
json:",omitempty",脱敏后为空就会整个字段消失;建议用"[REDACTED]"或固定长度占位符(如"***")
多层过滤要分清作用域和执行顺序
不是所有过滤都该塞进同一个中间件。比如 IP 黑名单、JWT 鉴权、字段脱敏,它们触发时机和依赖关系完全不同:
- IP 过滤必须最早执行(在路由匹配前),否则恶意请求可能绕过鉴权直接打到 handler
- JWT 鉴权应在路由匹配后、handler 执行前,方便按 group 或 path 精细控制
- 字段脱敏必须最后执行(在 handler 返回后、响应写出前),且仅作用于 JSON 响应
- 别把日志中间件放在脱敏之后——你记录的应该是原始响应体,不是脱敏后的,否则排查问题时看不到真实数据
最易被忽略的是:不同过滤层之间没有共享状态。比如 IP 黑名单中间件设了 c.Set("blocked", true),后续中间件得显式检查,不能默认跳过;而脱敏逻辑又依赖 handler 是否设置了 Data 字段,这些衔接点稍不注意就会漏掉一层过滤。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











