Nonce必须用Redis SETNX原子写入,避免GET+SET竞态导致防重放失效;key需含用户标识,TTL应匹配时钟偏差与网络延迟,推荐60–120秒;客户端传x-nonce头并参与签名,服务端先校验时间戳再查Redis。

Nonce必须用Redis SETNX原子写入,不能先GET再SET
竞态窗口是生产环境里最隐蔽的漏点:两个相同nonce请求几乎同时到达,若先GET查缓存返回不存在,接着都执行SET,结果就是两次都写成功。防重放彻底失效。
正确姿势是只用一条命令:SET nonce:uid123:abc123 1 EX 300 NX。Go 客户端推荐调rdb.SetNX(ctx, key, "1", 300*time.Second),返回true才代表首次注册;返回false或报错redis.ErrNil,直接拒掉请求(HTTP 409)。
- key 必须含用户标识,如
fmt.Sprintf("nonce:%s:%s", userID, req.Nonce),避免跨用户碰撞 - 不要用
INCR或EXISTS + SET组合——它们不是原子的 - 若用 Lua 脚本,确保
EVAL内完成判重+写入+过期,且脚本不依赖外部状态
过期时间不是越长越好,要匹配业务时钟偏差与网络延迟
设成 5 分钟(300 秒)是常见误区。真正该取值是:max(客户端与服务端最大时钟偏差, 请求最大网络往返时延 + 服务端处理耗时) + 缓冲(比如 30 秒)。
例如:客户端 NTP 同步误差 ±2 秒,CDN 到网关平均 RTT 150ms,后端处理峰值 800ms,则最小安全 TTL 是 2 + 0.15 + 0.8 ≈ 3 秒,但实际应设为 60–120 秒。设太高(如 3600 秒)会导致旧nonce长期滞留 Redis,增加内存压力和扫描开销;设太低(如 10 秒)则高延迟链路下的合法请求频繁被误拒。
立即学习“go语言免费学习笔记(深入)”;
- 毫秒级
timestamp(如time.Now().UnixMilli())可放宽对时钟精度的要求,但nonce本身仍需独立过期控制 - 不要依赖 Redis 自动过期兜底——实例重启后缓存全失,必须靠
nonce内嵌时间戳做二次校验 - 若业务允许宽松策略,可用
req.Timestamp参与 key 构造(如nonce:uid123:1718506560),按小时分片清理,降低单 key 压力
本地内存方案仅限单机低 QPS 场景,且必须手动定时清理
没 Redis 时,可用sync.RWMutex保护的map[string]time.Time做滑动窗口,但sync.Map不行——它不支持按值遍历删除,无法实现过期清理。
必须起一个 goroutine,定期(如每 15–30 秒)遍历 map,删掉time.Since(v) > window的条目。窗口大小建议 ≤ 60 秒,否则内存增长不可控。
- key 可用
sha256.Sum256([]byte(userID + ":" + nonce)).Hex()[:16]缩短长度,避免长字符串哈希冲突 - 禁止用
rand.Intn生成nonce——必须用crypto/rand.Read获取真随机源 - QPS 超过 500 或部署多实例时,此方案立即失效;它只适合内部工具、调试接口等非核心链路
客户端传X-Nonce比 Query 更安全,且签名原文必须包含它
把nonce塞进 URL query 会带来三重风险:被 CDN/代理记录、参与 GET 缓存键计算、日志系统明文落盘。Header 传输更可控。
Go 服务端读取时用r.Header.Get("X-Nonce"),注意 HTTP/2 下 Header 名自动小写,X-Nonce会被规范化为x-nonce,下游中间件若严格区分大小写可能出问题,建议统一约定小写x-nonce。
-
nonce必须参与签名原文拼接,例如method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + body_hash - 客户端生成推荐
base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%d-%s", time.Now().UnixNano(), uuid.NewString()))),纳秒时间戳 + UUID 保证唯一性与不可预测性 - 服务端解析
nonce时,若其中时间戳已超 5 分钟,直接拒绝,不走 Redis 查重——减少无效 IO


















