审计日志中间件必须在鉴权之后、业务handler之前注册,且置于gin.Recovery()之后;仅记录POST/PUT/DELETE/PATCH请求;严格采集六要素(UserIP、UserID、Action、ResourceID、Status、Timestamp);敏感字段需掩码并保留哈希供溯源;日志须独立输出隔离存储。

审计日志中间件必须在鉴权之后、业务 handler 之前注册
没拿到 userID 就记日志,等于给空账号打操作记录,审计时查不到人。Gin 中间件链是洋葱模型,顺序错了整个链就失效。
- 鉴权中间件(如 JWT 解析)必须
Use()在审计中间件之前,确保c.Get("user_id")能取到真实值 - 审计中间件必须放在
gin.Recovery()之后——否则 panic 时日志根本没写出来,只留个 500 错误,无从追溯 - 别把审计中间件挂到全局
r.Use()上再排除登录接口;用分组路由更安全:api := r.Group("/api"); api.Use(auth, audit) - 如果用了
chi或其它路由库,注意chi.URLParam(r, "id")必须在路径匹配后才有效,中间件里调用前得确认路由已解析
只记录 POST/PUT/DELETE,跳过 GET 和 OPTIONS
GET 请求本质是查询,不是“操作”,记多了全是噪音,还可能意外暴露敏感参数。审计日志的核心是“谁改了什么”,不是“谁看了什么”。
- 用
c.Request.Method显式过滤:if !slices.Contains([]string{"POST", "PUT", "DELETE", "PATCH"}, c.Request.Method) - 特别注意
OPTIONS:反向代理或 CORS 预检会发它,不跳过会导致大量无效日志 - 不要靠路径判断是否敏感(比如认为
/api/users就一定该记),而要结合方法 + 路径语义:同个路径下GET /orders不记,DELETE /orders/123必须记 - 对
PATCH要做业务映射:比如PATCH /api/orders/456实际是“确认收货”,日志里写"action": "order:confirm_receipt",而不是笼统的"patch"
关键字段必须从请求上下文提取,不能硬编码或漏采
审计日志六要素缺一不可:UserIP、UserID、Action、ResourceID、Status、Timestamp。少一个,等保或 ISO27001 审核就卡住。
-
UserIP别用c.ClientIP(),它不可信;优先取c.Request.Header.Get("X-Real-IP"), fallback 到c.Request.RemoteAddr(但需清洗端口) -
ResourceID必须是业务实体 ID,不是 URL 模板:从r.URL.Query().Get("file_id")或chi.URLParam(c, "id")提取,不是记/api/files/{id} -
Status记响应码不够,得记操作结果:成功时写200,失败时写err.Code(如"permission_denied")或自定义错误码,不能只写"failed" -
Timestamp必须在中间件入口统一采集:start := time.Now(),不能在c.Next()后才取,否则并发请求时间戳会错位
敏感字段脱敏要保留可追溯性,不能一刀切删光
脱敏不是抹掉一切,而是让日志既能满足合规要求,又能在出事时快速定位。全删=无法追责,全留=直接违规。
立即学习“go语言免费学习笔记(深入)”;
- 密码、token、手机号、身份证号必须掩码:
"phone": "138****1234",但额外记录"phone_hash": sha256("13812345678")[:12]供紧急溯源 - 文件路径、SQL 语句只记抽象标识:
"resource": "file:abc123",不记/var/data/uploads/xxx.pdf;SQL 记"query": "UPDATE users SET status=? WHERE id=?",不带实际参数值 - 临时 presigned URL 记哈希前缀:
"url_hash": sha256(url)[:6],不存全文,避免泄露签名密钥 - 日志输出必须走独立流:用
lumberjack.Logger写入/var/log/app/audit.log,MaxAge: 180,和业务日志物理隔离——混在一起,运维没法单独归档,安全团队 grep 效率极低


















