单纯用 time.now().unix() 校验时间戳无法防重放攻击,因它只判断时效性而不识别重复请求;必须结合 nonce 与 redis 原子校验,确保 timestamp 和 nonce 共同参与签名并一次性消费。

只在 Gin 中校验 timestamp 参数,不结合 nonce 和 Redis 原子校验,防不住重放攻击——攻击者只要在时间窗口内把抓到的合法请求原样重发一次,就成功了。
为什么单纯用 time.Now().Unix() 校验时间戳不行
时间戳本身只是个“有效期开关”,它只能拦住明显过期或未来时间的请求,但无法识别“同一份合法签名被重复提交”。常见错误包括:
- 客户端传
timestamp=1723385520,服务端只做if time.Now().Unix()-req.Timestamp > 300判断 - 没校验 timestamp 是否为未来时间(允许最多 10 秒误差即可,太多会放大时钟漂移风险)
- 没统一时间单位:前端用毫秒、后端用秒,导致恒不匹配
- 没对 timestamp 做参数存在性检查,攻击者直接删掉该字段绕过校验
Gin 中实现 timestamp + nonce 的最小可行校验链
必须让时间戳和随机数共同参与签名,并在服务端原子性验证两者。Gin 中推荐这样组织中间件逻辑:
- 从
X-Request-Timestampheader 或 query 中提取timestamp,强制要求非空且为整数 - 从
X-Request-Nonceheader 提取nonce,长度建议 32 字符(如 UUID v4 的 hex 形式) - 调用
redis.SetNX(ctx, "nonce:"+nonce, "1", 300*time.Second),返回false即拒绝请求 - 签名原文拼接顺序必须固定:例如
method + path + timestamp + nonce + body,body 需提前缓存(用gin.BindJSON前先读取并重置Request.Body) - 所有校验失败统一返回
http.StatusUnauthorized,不暴露是 timestamp 还是 nonce 出问题
容易踩的 Gin 特定坑
Gin 的上下文生命周期和中间件执行顺序会让一些看似合理的写法失效:
-
c.Request.URL.Query().Get("timestamp")在 POST JSON 场景下拿不到值——应优先从 header 读,或统一约定放在 query(仅限 GET)或 body(需解析前缓存) - 用
c.Next()后再校验 timestamp,会导致业务 handler 已经执行完毕——校验必须放在c.Next()前 - 本地内存模拟 nonce(比如 map + sync.RWMutex)在多实例部署下完全无效,别在生产环境这么干
- 没设置
time.Local或time.UTC一致,导致本地开发机和服务器时间解析偏差
真正起作用的不是 timestamp 本身,而是它和 nonce 在签名原文中的绑定关系,以及 Redis 对 nonce 的一次性消费保障。漏掉任意一环,重放防护就形同虚设。











