重放攻击的本质是合法签名的旧请求被截获后再次提交;仅校验时间戳无法防范,因签名和时间戳在有效期内可重复使用,必须结合nonce或滑动窗口等不可复用状态来确保请求唯一性。

重放攻击的本质是什么,为什么 Go 里不能只靠时间戳判断
重放攻击不是“接口被调用多次”,而是“合法签名的旧请求被截获后再次提交”。Go 服务端如果只校验 timestamp 是否在 5 分钟内,攻击者只要在有效窗口内重发一次请求,就成功了——因为签名本身没变、时间戳也没超时。
真正要防的是“同一个请求体 + 签名组合只能被接受一次”。这意味着必须引入服务端可验证且不可复用的状态,比如 nonce(一次性随机数)或滑动窗口计数器。
- 仅校验
time.Now().Sub(req.Timestamp) 是无效防御 - nonce 必须绑定到用户/设备维度(如
user_id:nonce),不能全局共用 - 存储 nonce 需要 TTL,推荐用 Redis 的
SET key value EX 300 NX原子操作,避免竞态
用 Redis 实现带过期的 nonce 校验(Go + redigo 示例)
这是生产中最常用、兼顾性能与安全的做法。关键不是“存 nonce”,而是“存的同时确保它没被用过,且自动过期”。
示例逻辑(省略错误处理):
立即学习“go语言免费学习笔记(深入)”;
conn := redisPool.Get()
defer conn.Close()
<p>key := fmt.Sprintf("nonce:%s:%s", userID, req.Nonce)
// NX = only set if not exists, EX = expire in 5 minutes
_, 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
}
- 不要先
GET再SET:中间有竞态窗口 - key 中必须含
userID或app_key,否则不同用户可能误撞 nonce - 过期时间(300 秒)应 ≥ 请求最大网络延迟 + 服务处理耗时,但不宜超过业务允许的最大时钟偏差
当无法用 Redis 时,如何用本地内存 + 时间窗口做轻量防重放
适合单机部署、QPS 不高、且能接受极小概率漏判的场景(如内部工具 API)。核心是维护一个带时间戳的滑动窗口 map,定期清理。
注意:sync.Map 不支持按值删除,所以得自己加锁 + 定时 goroutine 清理:
type ReplayCache struct {
mu sync.RWMutex
cache map[string]time.Time // nonce -> received time
window time.Duration
}
<p>func (c *ReplayCache) Check(nonce string) bool {
c.mu.RLock()
t, ok := c.cache[nonce]
c.mu.RUnlock()
if ok && time.Since(t) < c.window {
return false // 已存在且未过期 → 重放
}</p><pre class="brush:php;toolbar:false;">c.mu.Lock()
c.cache[nonce] = time.Now()
c.mu.Unlock()
return true}
- 不建议用
time.AfterFunc逐个清理,容易堆积;改用定时全量扫描 + 删除过期项 - 这个方案在多实例部署下完全失效,必须配合分布式存储
- 如果请求带
timestamp,可把 key 改为nonce + timestamp/60(按分钟分桶),减少 map 大小
签名算法里漏掉哪些字段会导致防重放失效
防重放依赖“签名覆盖所有可变字段”。只要签名没涵盖 nonce 或 timestamp,攻击者就能随意替换它们,签名依然有效。
- 签名原文必须包含:
method+path+body(或 body hash)+timestamp+nonce+app_key - 忽略
Content-Type或Host可能导致代理层篡改后签名仍通过 - body 如果是 form-data 或 multipart,要规范化后再哈希,否则空格/换行差异会破坏一致性
- Go 的
http.Request.Body默认只能读一次,签名前需用io.TeeReader或bytes.Buffer缓存,否则后续解析失败
防重放最易被忽略的点是:把 nonce 当成客户端生成的“装饰字段”,而没强制要求它参与签名、也没在服务端绑定身份和时效。一旦这两条断开,整个机制就退化成心理安慰。


















