Iris框架无内置脱敏能力,需在ctx.JSON()前手动干预或用自定义MarshalJSON、zerolog.Hook实现;中间件无法统一拦截响应体因ResponseWriter写入后不可读取,且结构体未序列化时字段敏感性未知。

Iris 框架本身不提供内置脱敏能力,必须结合日志库(如 zerolog)或手动序列化控制实现,且脱敏逻辑不能只靠中间件“一贴了事”——字段是否敏感、脱敏粒度、是否影响下游消费,全得在 Context.JSON() 或 Context.XML() 之前明确干预。
为什么不能用中间件统一拦截响应体
因为 Iris 的 Context.ResponseWriter 是 http.ResponseWriter 的包装,写入后无法再读取或修改原始字节流;中间件拿到的只是未序列化的 Go 结构体(如果用了 ctx.JSON()),但此时结构体还没转成 JSON 字符串,你无从知道哪些字段该脱敏——除非你强制所有 API 返回统一包装类型(如 Response{Data: interface{}, Code: int}),并在这个包装层做字段遍历。但这样会破坏类型安全,且对 Swagger 文档生成、客户端解码都造成干扰。
- 常见错误现象:
iris.Context的WriteHeader+Write直接输出 raw bytes,中间件完全不可见 - 真正能插手的时机只有两个:返回前调用
ctx.JSON()时传入已脱敏结构体,或用自定义json.Marshaler接口 - 性能影响:反射遍历字段做脱敏(尤其嵌套深、字段多)比直接序列化慢 3–5 倍,线上高 QPS 接口慎用
用 zerolog.Hook 实现日志脱敏,但不影响 API 输出
这是最轻量、最解耦的做法:仅确保日志里不泄露敏感字段,API 响应体保持原样(由业务决定是否脱敏)。Iris 默认不集成 zerolog,需手动挂载;关键点是 hook 必须在 zerolog.New() 之后、iris.Logger().SetOutput() 之前注册。
- 使用场景:调试阶段看日志查问题,但又不想在日志里看到
password、id_card、access_token - 参数差异:
zerolog.Event.Fields()返回的是[]interface{},需按 key-value 成对解析,不能直接用map[string]interface{}断言 - 示例中
maskSensitiveFields(e)应检查每个 field 的 key 是否匹配["password", "token", "phone"]等白名单,值为字符串时替换为"***",为 struct 时递归处理
在结构体上用 json tag 控制字段输出与脱敏
最可控的方式:把脱敏逻辑下沉到数据模型本身,用 json: tag 和自定义 MarshalJSON 方法分离“展示值”和“原始值”。比如用户结构体里存原始手机号,但 JSON 输出时自动遮蔽中间四位。
- 常见错误现象:直接在 handler 里
user.Phone = "***" + user.Phone[7:]—— 这会污染原始数据,后续其他逻辑(如发短信、审计)可能出错 - 正确做法:定义
type User struct { Phone string `json:"phone"` },再为User实现MarshalJSON(),内部构造临时 map,对Phone做掩码后再序列化 - 兼容性注意:Swagger 自动生成文档时不会识别
MarshalJSON逻辑,仍显示原始字段类型,需额外用// @Schema注释补充说明
用自定义 Context.JSON 封装脱敏逻辑
如果你有大量接口需要统一脱敏规则(如所有 /api/v1/** 路径下响应中的 id_card 字段必须掩码),可以封装一个 ctx.SafeJSON(code int, v interface{}) 方法,在里面做字段级扫描和替换。但必须满足两个前提:v 是 struct 或 map,且字段名可预测(不能是 interface{})。
- 容易踩的坑:对 slice 或嵌套 struct 递归脱敏时,没做类型判断直接
reflect.ValueOf(v).Interface(),导致 panic - 性能建议:缓存反射结果(
reflect.TypeOf(v)),避免每次调用都重复解析结构体布局 - 不要试图脱敏
time.Time、float64等基础类型——它们不可能是敏感字段,强行检查反而拖慢响应
真正难的不是写脱敏代码,而是确定哪些字段在什么上下文里算“敏感”:同一个 email 字段,在用户个人页要完整展示,在运营后台导出报表时就得掩码。所以脱敏逻辑最好能按路由路径、角色权限、甚至请求头里的 X-Debug: true 动态开关,而不是写死在结构体或全局 hook 里。


















