hmac-sha256 + timestamp 仍会被重放,因仅校验时间差无法阻止同一合法请求在窗口内重复提交;必须用 nonce 结合 userid 做原子性已消费校验,推荐 redis set key value ex ttl nx,timestamp 和 nonce 需明文传输并参与签名。

只对参数哈希无法防重放,必须让每个请求在服务端可验证且不可复用——timestamp 和 nonce 必须参与签名计算,并在服务端原子性校验是否已消费。
为什么 HMAC-SHA256 + timestamp 仍会被重放
常见错误是把 timestamp 放进签名原文,但服务端只做 time.Now().Unix() - req.Timestamp 校验。这完全无效:攻击者截获一次合法请求(含签名、timestamp、nonce),只要在时间窗口内原样重发,HMAC 验证通过、时间差也合规,请求就被重复执行。
真正要拦住的是“同一签名+参数组合只能被接受一次”。这意味着服务端必须维护一个已消费状态,且该状态需满足三个条件:唯一标识请求、带 TTL 自动过期、写入操作必须原子。
-
timestamp单独存在只是时效性控制,不是防重放的充分条件 -
nonce若未绑定用户维度(如user_id:nonce),不同用户可能撞值,导致误拒或漏检 - 若用
GET先查nonce是否存在、再SET,中间存在竞态窗口,两个并发请求可能同时通过校验
Go 中用 Redis 实现原子 nonce 校验
生产环境最可靠的做法是用 Redis 的 SET key value EX 300 NX 命令,它在一个原子操作里完成“不存在才设置 + 设置即过期”两件事。
示例关键逻辑(使用 github.com/gomodule/redigo/redis):
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
key := fmt.Sprintf("nonce:%s:%s", userID, req.Nonce)
_, err := conn.Do("SET", key, "1", "EX", 300, "NX")
if err != nil {
if err == redis.ErrNil {
return errors.New("replay detected: nonce already used")
}
return err
}
-
EX 300表示 5 分钟过期,应 ≥ 客户端最大网络延迟 + 服务处理耗时,但不宜超过允许的最大时钟偏差(比如客户端和服务端时间差 ≤ 2 分钟,则设为 300 秒偏大,240 更稳妥) -
NX是核心,禁止用GET+SET替代,否则高并发下必然漏判 -
userID必须来自可信上下文(如 JWT 解析后取sub字段),不能从请求参数直接拼接,防止伪造
客户端生成 timestamp 和 nonce 的硬性要求
这两项必须明文传给服务端(建议放在 X-Timestamp 和 X-Nonce header),否则服务端无法提取并参与验签和去重。
-
timestamp推荐用time.Now().UnixMilli()(毫秒级整数),比Unix()更抗时钟漂移;服务端解析时也必须用strconv.ParseInt转成 int64,避免浮点或字符串格式歧义 -
nonce每次请求都必须重新生成,用crypto/rand.Read(buf)生成 16 字节随机数,再调用hex.EncodeToString(buf);禁止用 UUID 或时间戳拼接,它们不具备密码学随机性 - 签名原文拼接顺序必须严格一致,例如:
method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + body,其中body为空也要保留换行,JSON body 禁用缩进
没有 Redis 时的本地降级方案及其边界
仅适用于单机部署、QPS
核心是维护一个带时间戳的 map[string]time.Time,用 sync.RWMutex 保护读写,不能用 sync.Map(不支持按值删除):
- 每次请求先加读锁查
nonce是否存在且未过期(time.Since(t) ) - 若不存在,加写锁写入
nonce: time.Now() - 另起 goroutine 每 30 秒遍历 map 清理过期项,注意遍历时需复制 key 列表避免 panic
- 窗口期(如 300 秒)必须和服务端时间校验窗口一致,否则出现“已过期但未清理”的误判
这个方案最难缠的点不在实现,而在时钟同步和清理时机——哪怕服务端时间比客户端快 2 秒,就可能把刚写入的 nonce 当作过期清理掉,导致合法请求被拒。真实线上环境,别省这个 Redis。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










