防重放模块必须原子校验 timestamp + nonce + 签名,三者需在一次 redis 操作中完成,单独校验任一字段均失效,否则攻击者可微调 timestamp 并重放原 nonce。

防重放模块必须原子校验 timestamp + nonce + 签名
单独校验时间戳或单独查 nonce 都会失效——攻击者可先截获请求,微调 timestamp 绕过窗口检查,再用原 nonce 重放。三者必须在一次 Redis 操作中完成:先验证 abs(now - timestamp) (单位秒),再用 <code>SET nonce:uid123:abc123 1 EX 60 NX 原子注册。失败即返回 HTTP 409 Conflict。
常见错误:
- 把
timestamp放在X-Timestamp头里但没参与签名计算 - 用
time.Now().UnixMilli()生成毫秒时间戳,服务端却用Unix()解析,导致校验永远失败 - nonce key 没带用户 ID,多个用户共用同一 nonce 导致误拒或漏检
nonce 存储必须用 Redis SETNX,禁用 GET+SET 组合
竞态是生产环境最常被忽略的点:两个相同 nonce 请求并发到达,都执行 GET nonce:xxx 得到空值,接着都 SET 成功,防重放完全失效。
Go 客户端必须用原子命令:
-
rdb.SetNX(ctx, key, "1", 60*time.Second)—— 推荐,语义清晰 - 或 Lua 脚本:
EVAL "if redis.call('exists', KEYS[1]) == 0 then return redis.call('setex', KEYS[1], ARGV[1], '1') else return 0 end" 1 nonce:uid:abc 60 - 绝对不要用
rdb.Exists(ctx, key).Val() == 0后再Set—— 中间存在不可控时间窗口
签名原文拼接顺序和 body 处理极易不一致
客户端和服务端对签名原文的构造必须一字不差,否则 HMAC 对不上。典型约定是:
method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + body-
method必须大写("POST"),path不含 query string -
body是原始字节流:JSON 不加缩进、不自动排序 key;空 body 也要保留末尾换行 - timestamp 必须是 Unix 秒级整数字符串(
strconv.FormatInt(time.Now().Unix(), 10)),不能是浮点或带毫秒
示例签名函数关键行:
io.WriteString(h, method+"\n")<br>io.WriteString(h, path+"\n")<br>io.WriteString(h, timestamp+"\n")<br>io.WriteString(h, nonce+"\n")<br>io.WriteString(h, body)
没有 Redis 时的降级方案只适用于低 QPS 单机场景
用 sync.RWMutex 包裹 map[string]time.Time 可临时支撑测试或内部工具,但必须自己实现定时清理过期项——sync.Map 不支持遍历,无法做 TTL 清理。
降级方案限制明显:
- 无法跨进程去重,多实例部署直接失效
- map 不清理会持续增长,OOM 风险随运行时间升高
- 滑动窗口逻辑需手动维护,容易漏掉并发写入冲突
真正上线前,Redis 或其他分布式锁存储不是可选项,而是必要前提。nonce 的全局唯一性一旦破坏,整个防重放机制就形同虚设。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











