单纯校验时间戳不能防重放,必须结合nonce和redis原子操作做请求指纹去重;因攻击者可在时间窗口内原样重放合法请求,服务端缺乏“是否已被消费”的状态标记,而时间戳仅约束时效性、非唯一性。

单纯校验时间戳不能防重放,必须结合 nonce 和 Redis 原子操作做请求指纹去重。
为什么只校验 X-Timestamp 会失效
攻击者截获一次合法请求后,在 time.Now().Unix() - req.Timestamp 的窗口内原样重发,签名仍有效、时间仍在范围内——服务端无法区分这是“新请求”还是“旧包重放”。关键缺失的是“该请求是否已被消费过”的状态标记。
- 时间戳只是时效性约束,不是唯一性约束
- 客户端若用本地时间(未同步 NTP),
time.Now().Unix()和服务端偏差可能超窗口,导致误拒 - 建议统一用毫秒级时间戳:
time.Now().UnixMilli(),降低时钟漂移影响 - 但毫秒级 timestamp 仍需配合
nonce,否则高并发下仍可能撞窗重放
如何用 redis.SetNX 做原子 nonce 校验
核心是构造带用户维度的 key,并用单命令完成“检查 + 写入 + 过期”,避免 GET + SET 的竞态窗口。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- key 格式推荐:
fmt.Sprintf("replay:%s:%s", userID, req.Nonce),绑定用户防止跨账号碰撞 - 过期时间设为
60 * time.Second:覆盖网络 RTT(≤200ms)+ 服务处理峰值(≤800ms)+ 时钟偏差(≤2s)+ 缓冲(≈57s) - 务必用
rdb.SetNX(ctx, key, "1", 60*time.Second),返回false或redis.ErrNil即表示已存在,直接拒绝 - 不要用
INCR或 Lua 脚本封装EXISTS+SET——它们不保证原子性,除非脚本内完整实现判重逻辑
Gin 中间件里怎么安全提取和校验
签名验证、时间戳检查、nonce 去重这三步必须在同一个中间件里串行执行,且 nonce 校验必须在验签通过之后、业务逻辑之前。
- 先从
c.Request.Header.Get("X-Timestamp")和c.Request.Header.Get("X-Nonce")提取,缺一不可 - 解析 timestamp 时用
strconv.ParseInt,拒绝非数字或负值 - body 需提前读取一次:
body, _ := io.ReadAll(c.Request.Body),再用c.Request.Body = io.NopCloser(bytes.NewBuffer(body))复位,否则下游c.BindJSON会失败 - 签名原文拼接顺序必须严格一致,例如:
method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + string(body) - 验签通过后再跑
SetNX,否则攻击者可绕过签名直接刷 nonce 校验
没 Redis 时怎么降级(仅限单机低 QPS)
可用 sync.RWMutex + map[string]time.Time 实现滑动窗口,但必须手动清理过期项——sync.Map 不支持遍历删除,不能用。
- key 用
userID + ":" + nonce,value 存time.Now() - 每次写入前,先锁住 map,遍历删掉
time.Since(v) > 60 * time.Second的项 - 起 goroutine 每 30 秒扫一次全量 map 清理,避免长连接积压
- 该方案在多实例部署下完全失效,QPS 超 50 就容易因锁争用拖慢整体吞吐
真正难的不是写对那几行 SetNX,而是 timestamp 和 nonce 在签名原文里怎么拼、header 怎么传、body 怎么复用、Redis key 怎么设计——漏掉任意一环,防重放就形同虚设。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!










