只校验timestamp会放行重放攻击,因它仅验证请求是否“不过期”,无法识别“同一合法请求是否已被处理过”;必须结合带用户维度的nonce与redis原子操作(set key ex nx)确保签名+参数组合仅一次有效。

为什么只校验 timestamp 会放行重放请求
攻击者截获一次合法请求(含正确签名、timestamp、nonce),只要在服务端设定的时间窗口内原样重发,HMAC 验证通过、time.Now().Unix() - req.Timestamp 也落在允许范围内,整个请求就被照单执行——这正是重放攻击成功的关键路径。
时间戳只解决“太旧的请求不处理”,不解决“同一合法请求是否已被处理过”。防重放的本质是拒绝“签名+参数+时间+随机数”这一整套组合的第二次出现。
- 仅用
timestamp校验,等于给攻击者开了一个固定时长的重放窗口 -
nonce若未绑定用户维度(如拼接userID),不同用户可能生成相同值,导致误拒或漏判 - 若
nonce存储不用原子操作(比如先GET再SET),高并发下两个请求可能同时读到“不存在”,然后都写入,彻底失效
用 Redis 的 SET key value EX ttl NX 做原子校验
这是生产环境最轻量且可靠的落地方式。它在一个命令里完成“判断是否存在 + 不存在才设置 + 设置即带过期”,没有竞态窗口。
关键点不是“存 nonce”,而是“存的同时确保它没被用过,且自动过期”:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
key必须带用户粒度,例如fmt.Sprintf("nonce:%s:%s", userID, req.Nonce),避免全局冲突 -
EX值应 ≥ 客户端最大网络延迟 + 服务端处理耗时,但不宜超过业务允许的最大时钟偏差(比如客户端和服务端时间差 ≤ 2 分钟,则设EX 240比EX 300更稳妥) - 返回
redis.ErrNil表示 key 已存在 → 重放已发生,直接拒绝;其他错误需按实际处理(如连接失败应记录并允许降级) -
userID必须来自可信上下文(如 JWT 解析后的sub字段),不能从请求参数中直接取,否则可被伪造
客户端必须明文传 X-Timestamp 和 X-Nonce
这两项不能只塞进签名原文里不暴露,否则服务端无法提取并参与验签和去重校验。
-
timestamp推荐用time.Now().UnixMilli()(毫秒级整数),比Unix()更抗时钟漂移;服务端解析时也必须用毫秒,避免单位错位 -
nonce必须每次请求都重新生成,用crypto/rand.Read()生成 16 字节随机数,再转hex.EncodeToString()或base64.StdEncoding.EncodeToString() - 常见错误:把
nonce写死、或只传timestamp不传nonce、或签名原文拼接顺序与服务端不一致(比如客户端拼method+path+body+timestamp+nonce,服务端却按method+path+timestamp+nonce+body)
没有 Redis 时的本地滑动窗口降级方案
仅适用于单机部署、QPS 较低、且能接受极小概率漏判的内部工具类 API。
- 用
sync.RWMutex保护一个map[string]time.Time,key 是userID:nonce或sha256.Sum256(body)[:8](省去传参) - 不能用
sync.Map—— 它不支持按值删除,无法实现“过期自动清理” - 需起一个 goroutine 定期(如每 30 秒)遍历 map,删掉
time.Since(t) > window的条目 - 多实例部署下该方案完全失效,因为状态不共享;一旦上生产,必须切回 Redis 或数据库方案
真正难的不是写对那一行 conn.Do("SET", ...),而是保证客户端和服务端对 timestamp 单位、nonce 生成方式、签名原文拼接顺序、userID 来源这四点完全一致——任何一处脱节,验签或去重就会静默失败。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










