仅校验时间戳无法防御重放攻击,必须结合nonce或滑动窗口实现“签名+参数组合仅一次有效”;推荐用Redis的SET key value EX 300 NX原子操作校验带用户维度的nonce。

只校验时间戳是无效的防重放——攻击者在5分钟窗口内重发一次合法请求就成功了。真正要拦住的是“同一个签名+参数组合只能被接受一次”,必须引入服务端可验证、不可复用的状态,比如 nonce 或滑动窗口。
为什么 time.Now().Sub(req.Timestamp) 校验完全不够用
重放攻击不是“接口被调用多次”,而是“合法签名的旧请求被截获后再次提交”。如果只检查 timestamp 是否在 ±180 秒内:
- 签名原文含
timestamp和nonce,HMAC 验证必然通过 - 时间仍在有效区间,时间校验也通过
- 但这个请求本应只处理一次——服务端缺少“已消费”状态标记
结果就是:攻击者抓包后原样重发,服务端照单全收。
必须用 Redis 的 SET key value EX 300 NX 做原子 nonce 校验
这是生产环境最可靠、最轻量的落地方式。关键不是“存 nonce”,而是“存的同时确保它没被用过,且自动过期”:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
-
key必须带用户/设备维度,例如fmt.Sprintf("nonce:%s:%s", userID, req.Nonce),避免不同用户撞nonce - 务必用
NX(仅当 key 不存在时设置),禁止先GET再SET——中间有竞态窗口 -
EX 300表示 5 分钟过期,应 ≥ 网络最大延迟 + 服务处理耗时,但不宜超过业务允许的最大时钟偏差(如客户端与服务器时间差) - 若
conn.Do("SET", ...)返回redis.ErrNil,说明 key 已存在 → 重放已发生,直接拒绝
客户端怎么生成和传 timestamp 与 nonce
这两项不能只塞进签名原文里不暴露,必须作为独立 HTTP 参数(如 header 或 query)明文传输,否则服务端无法提取并校验:
-
timestamp推荐用time.Now().UnixMilli()(毫秒级),比秒级更抗时钟漂移 -
nonce必须每次请求都重新生成,用crypto/rand.Read()生成 16 字节随机数,再转hex.EncodeToString或base64.StdEncoding.EncodeToString - 常见错误:把
nonce写死在客户端、或只用时间戳不用nonce、或签名原文拼接顺序不一致导致服务端验签失败
没有 Redis 时的降级方案:本地滑动窗口 + 定时清理
仅适用于单机部署、QPS 较低、且能接受极小概率漏判的内部工具类 API:
- 维护一个带时间戳的
map[string]time.Time,用sync.RWMutex保护读写 - 不能用
sync.Map——它不支持按值删除,无法实现“过期自动清理” - 需起一个 goroutine 定期(如每 30 秒)遍历 map,删掉
time.Since(t) > window的条目
多实例部署下该方案会彻底失效;高并发时 map 锁竞争也会成为瓶颈。真要上生产,别省那点 Redis 资源。

















