时间戳+签名组合不能直接防重放,因为仅校验时间窗口和签名正确性无法识别已处理过的请求;必须引入nonce等唯一性标识并配合服务端存储(如Redis)进行一次性校验,且签名须覆盖timestamp和nonce等防重放字段,否则攻击者可在有效期内重复发送合法请求。

为什么时间戳+签名组合不能直接防重放
单纯用 time.Now().Unix() 生成时间戳再签名,攻击者截获一次请求后,在有效窗口内(比如5秒)反复重放,服务端只要签名验签通过、时间戳在窗口内就放行——这根本没防住重放。关键不是“有没有签名”,而是“这个请求是否已被处理过”。
必须引入唯一性标识 + 存储校验
防重放的本质是拒绝已处理过的请求,所以每个请求得带一个全局唯一、不可预测、一次性有效的标识。常见做法是用 nonce(随机数)配合时间戳:
-
nonce由客户端生成,如hex.EncodeToString(randomBytes(16)),每次请求不同 - 服务端收到后,先检查
timestamp是否超窗(如abs(now - req.Timestamp) > 30),超窗直接拒 - 再查
nonce是否已在 Redis 中存在(key 可设为"replay:" + req.Nonce),存在则拒 - 若未存在,用
SET key "" EX 30 NX写入,NX 确保原子性,EX 保证自动过期(和时间窗对齐)
签名计算必须覆盖防重放字段
如果签名只签业务参数,不包含 timestamp 和 nonce,攻击者就能篡改这两个值绕过校验。正确做法是把防重放字段和业务参数一起序列化后签名:
data := fmt.Sprintf("%d|%s|%s|%s", req.Timestamp, req.Nonce, req.UserID, req.OrderID)
sig := sign(data, privateKey)
服务端验签前也必须用同样顺序和格式拼接——字段顺序、分隔符、空值处理(如 "" 还是 "null")稍有不一致就会验签失败。
注意:不要用 JSON 序列化做拼接源,因键排序不确定;固定字段顺序最稳。
Redis 存储 nonce 的几个实际坑
看似简单,但线上常踩的点不少:
- Redis 连接池耗尽:高频微服务下,每个请求都打一次
SET ... NX EX,没加连接池或超时设置,会卡死整个服务 - 未设置
EX导致nonce永久占用内存;设太短(如 5 秒)又可能误杀正常延迟请求 - 没用
NX而用SET+GET两步,中间可能被并发请求穿透,导致重复消费 - Key 过长或含特殊字符(如
/、空格)引发 Redis 协议解析异常,建议只保留字母数字和短横线
真正上线时,nonce 生命周期、Redis 响应超时、失败降级策略(比如临时跳过防重放但打告警)都得实打实配好,不能只靠“理论上安全”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











