真正有效的防重放方案必须同时满足三个条件:签名含timestamp和nonce、服务端原子性校验二者、且nonce按用户维度存Redis并带TTL;timestamp须为秒级整数,nonce推荐16字节随机hex,拼接顺序严格固定为method+"\n"+path+"\n"+timestamp+"\n"+nonce+"\n"+body;验签中间件须原子执行时间差检查、nonce去重与验签三步,Redis key格式为"nonce:userID:nonce"并设TTL略大于窗口期;客户端须同步NTP、独立传输X-Timestamp/X-Nonce头,严禁复用nonce。

只校验时间戳或只验签名,都防不住重放攻击。真正有效的方案必须同时满足三个条件:签名含 timestamp 和 nonce、服务端原子性校验二者、且 nonce 按用户维度存 Redis 并带 TTL。
签名原文必须包含 timestamp 和 nonce,且顺序固定
常见错误是把 timestamp 放在 Header 里却不参与签名计算,或者拼接顺序不一致(比如客户端用 method+path+body,服务端却加了 nonce)。这会导致验签必败。
-
timestamp必须是秒级整数(time.Now().Unix()),不能是毫秒或字符串,避免解析歧义 -
nonce推荐用 16 字节随机数转 hex:hex.EncodeToString(randBytes),长度固定便于索引 - 拼接顺序要严格约定,例如:
method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + body(body为空也保留换行) - HTTP 方法名必须大写(
"POST"),path必须是原始路径(不含 query string),body必须是原始字节(JSON 禁用缩进)
验签中间件必须原子执行 nonce 校验
验签、时间差检查、nonce 去重这三步如果拆开,就会出现竞态:两个相同 nonce 请求可能同时通过“是否存在”查询,再同时写入,导致漏检。
- 必须用 Redis 的
SET key value EX 300 NX(或数据库的INSERT ... ON CONFLICT DO NOTHING)保证原子性 -
key必须带用户/设备维度,例如fmt.Sprintf("nonce:%s:%s", userID, req.Nonce),不能全局共用 - TTL 设为略大于窗口期(如 300 秒),既要覆盖网络延迟 + 处理耗时,又不能超过允许的最大时钟偏差
- 若
rdb.SetNX返回redis.ErrNil,说明已存在 → 直接拒绝,不要补查
客户端传参和时间同步容易被忽略
服务端再严密,客户端乱传也会让整套机制失效。最常踩的坑不是代码逻辑,而是传输层约定松散。
-
X-Timestamp和X-Nonce必须作为独立 HTTP Header 明文传递,不能塞进 URL 或 query string(会被 CDN 缓存或日志泄露) - 客户端必须同步 NTP 时间,否则
timestamp偏差 >30 秒就会频繁被拒;服务端校验时应允许 ±30 秒浮动,但不能放宽到分钟级 -
nonce每次请求都必须重新生成,严禁复用(包括静态字符串、递增整数、UUID 而不做全局查重) - 若用 JWT,
jti不能只当随机 ID 存,得按小时分片(如used_jti:2026070115),否则单 key 写入瓶颈明显
真正的难点不在签名函数怎么写,而在于 nonce 的生命周期管理——它必须短(否则失去防重放意义)、必须绑定用户(否则跨账号碰撞)、必须原子写入(否则并发下失效)。哪怕用了 Redis,如果 key 设计没带 user_id 或 TTL 设置不合理,照样被绕过。











