gin默认中间件不防重放攻击,需自研中间件实现时间窗口校验、nonce去重及签名验证三要素,且仅对非幂等请求启用。

单纯在 Gin 里校验 X-Timestamp 头或参数,完全防不住重放攻击——它只拦得了一次过期请求,拦不住同一份合法请求在有效期内被反复提交。
为什么 Gin 默认中间件不解决重放问题
Gin 本身没有内置防重放逻辑,gin.Default() 里的 Logger 和 Recovery 中间件只管日志和 panic 恢复,对时间戳、随机数、签名一概不碰。你必须自己写中间件,且要嵌在业务 handler 执行前(即 c.Next() 前),否则校验就失去意义。
- 用
c.ShouldBindJSON()读了c.Request.Body后,Body 流已关闭,后续再读会得到空内容——必须提前缓存并重置:bodyBytes, _ := io.ReadAll(c.Request.Body); c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes)) - 把 nonce 存本地
map[string]struct{}+sync.RWMutex,单机测试能过,多实例部署直接失效 - 用
c.Query("timestamp")提取时间戳,在 POST JSON 场景下永远拿不到值,因为 query 是 URL 参数,不是 body 里的字段
防重放中间件必须做的三件事
核心是原子性验证:时间窗口内 + 未使用过的 nonce + 签名一致。缺一不可。
- 从
X-Request-Timestamp或X-Timestamp请求头取毫秒级整数,拒绝非数字、空值、未来时间偏差 >10s 的请求 - 从
X-Nonce头取字符串(推荐 UUID v4 hex 格式,32 字符),拼进签名原文,再用redis.SetNX(ctx, "nonce:"+nonce, "1", 300*time.Second)写入;返回false表示已存在,立刻c.AbortWithStatus(401) - 签名原文固定顺序:
method + path + timestamp + nonce + normalized_body;其中normalized_body需去空格、统一换行、JSON 字段排序(可用jsoniter.ConfigCompatibleWithStdlib.SortMapKeys(true))
GET 接口要不要加 timestamp 校验
不要。GET 是幂等查询操作,按规范应跳过防重放校验。强行加只会让前端缓存失效、CDN 不生效、日志膨胀,还可能被绕过(比如改用 POST 模拟 GET)。
- 只对
POST、PUT、DELETE、PATCH启用该中间件 - 在中间件开头加判断:
if c.Request.Method == http.MethodGet || c.Request.Method == http.MethodHead { c.Next(); return } - 别用 query 传
timestamp做校验依据——它会被浏览器历史、代理、CDN 缓存,攻击者抓包后可无限重放
真正难的不是写完中间件,而是保证签名原文拼接顺序前后端严格一致、Redis 写入失败时有合理降级(比如只校验 timestamp)、密钥不硬编码进代码——这些细节一旦出错,防重放就形同虚设。











