Gin日志中间件不支持请求体或响应体脱敏,必须分两步:request脱敏在c.Next()前完成,response脱敏需替换c.Writer;gin.LoggerWithFormatter仅能处理元信息,无法访问原始body。

直接说结论:Gin 的日志中间件本身不支持请求体或响应体脱敏,必须拆成两步——request 脱敏在 c.Next() 前做,response 脱敏必须替换 c.Writer,日志格式化阶段只负责把已脱敏的数据打出来。硬塞进 gin.LoggerWithFormatter 里做字符串替换,100% 会漏字段、破 JSON、误杀路径或 header。
gin.LoggerWithFormatter 里只能处理已脱敏的字段
这个函数签名接收的是 gin.LogFormatterParams,它只包含请求方法、路径、状态码、耗时等元信息,没有原始请求体、也没有响应体内容。你没法在里面解析 POST /api/login 的 JSON body 或读取 c.Writer 缓存。
- 能安全用的字段只有:
c.Request.URL.Path、c.Request.Method、c.Keys(如果中间件提前塞了)、c.Writer.Status()等 -
c.Request.URL.RawQuery和c.Request.Header可以取,但要手动脱敏(比如遍历r.URL.Query(),再用r.URL.RawQuery = url.Values{...}.Encode()同步更新) - 别试图在 formatter 里调
io.ReadAll(c.Request.Body)—— 此时 body 已被前面中间件消费过,读出来是空的或 panic
request body 脱敏必须在 c.Next() 之前完成
因为 http.Request.Body 是单次读取流,gin 的 binding(如 c.ShouldBindJSON(&u))或你自己的 handler 会直接 consume 它。脱敏动作必须早于任何 binding 操作。
- 开头加
r.Body = http.MaxBytesReader(nil, r.Body, 10<<20)防 OOM - 用
bodyBytes, _ := io.ReadAll(r.Body)读一次,再用r.Body = io.NopCloser(bytes.NewReader(bodyBytes))重置 - 对
application/json类型,json.Unmarshal(bodyBytes, &raw)成map[string]interface{},递归检查 key 是否在敏感列表(如"password"、"token"、"idCard"),仅字符串值替换成"***" - GET 请求跳过 body 读取:
if r.Method != "GET" && r.ContentLength > 0 - 表单类请求(
application/x-www-form-urlencoded)优先走r.ParseForm(),从r.PostForm取值脱敏,别碰 raw body
response body 脱敏必须包装 c.Writer
gin 默认不缓存响应体,c.Writer.Write([]byte) 一调就发到 TCP 连接。等 c.Next() 返回再去读 c.Writer,什么都拿不到。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 定义一个
responseWriter结构体,嵌入gin.ResponseWriter,重写Write([]byte)和WriteHeader(int) - 只对
Content-Type: application/json做处理,其他类型(text/html、image/png)直接透传 - 在
Write()中,先判断是否首次写(!rw.written),是则缓存字节;等第二次写或WriteHeader()调用后,再解析 JSON、脱敏、写回原 writer - 推荐用
json.Decoder.Token()流式匹配 key 路径(如data.user.phone),避免全量Unmarshal占内存 - 若响应已是压缩(
Content-Encoding: gzip),不要尝试解压再改——风险高且无必要,这类响应应直通
结构体字段脱敏不能靠中间件自动识别
中间件看到的只是 *http.Request 和 http.ResponseWriter,它不知道 User.Phone 是手机号,也不知道 Order.IDCard 是身份证号。所有“自动扫描 password 字段”都是幻觉。
- 真正可控的是让业务结构体实现
json.Marshaler接口,例如:func (u *User) MarshalJSON() ([]byte, error) - 用别名类型防递归:
type userAlias User,再return json.Marshal((*userAlias)(u)) - 字段必须导出(首字母大写),否则反射不可见;避免在
MarshalJSON里解引用未判空的指针 - zap 日志场景下,结构体需实现
zapcore.ObjectMarshaler,而不是依赖全局 hook 做字符串替换 - zerolog 场景下,用
InterfaceHook封装SanitizeMap,只递归处理map[string]interface{},跳过int、time.Time等基础类型
最易被忽略的点:脱敏不是一次配置就能覆盖全部出口。HTTP 请求日志、gRPC 响应、后台任务输出、甚至配置加载(viper.Unmarshal)都得各自控制。字段级脱敏必须从序列化入口开始,而不是等数据流到日志库才动手。


















