单纯用time.time()生成时间戳防重放无效,因网络延迟、时钟漂移导致“时间新鲜”不等于“请求未重放”;必须结合nonce唯一性校验与缓存级原子写入(如Redis SET NX)确保每个timestamp+signature组合仅使用一次。

为什么单纯用 time.time() 生成 Token 时间戳不够防重放
因为网络延迟、客户端时钟漂移、服务端时钟不同步,会导致服务端收到请求时,timestamp 虽在“5分钟窗口内”,但该请求可能已被中间人截获并重复提交。关键不是“时间是否新鲜”,而是“这个时间戳+签名组合是否已被用过”。Django 默认的 django.contrib.auth.tokens.PasswordResetTokenGenerator 不保存使用记录,也不校验唯一性,直接复用它做防重放是无效的。
真正可行的做法是:把 Token 的核心标识(如 user_id + timestamp + salt)哈希后存入缓存或数据库,并设置过期时间;验证时先查是否存在,存在则拒绝,不存在则写入并放行。
如何用 cache.set() 实现轻量级防重放校验
Django 的 cache(推荐 RedisCache)天然支持原子性写入+过期,适合做一次性 Token 标识的临时存储。重点在于构造不可预测、不可重放的 key。
- 不要只用
timestamp做 key —— 攻击者可微调时间重发 - 推荐 key 格式:
f"replay:{user_id}:{int(timestamp)}:{random_string(8)}",其中random_string由服务端生成并随 Token 返回(避免客户端可控) - Token 本身应包含:
user_id、timestamp、random_string、signature(用hmac.new(key, msg, digestmod=sha256).hexdigest()签名) - 验证时:先计算预期 signature,不匹配直接拒绝;匹配则拼出完整 key,调用
cache.get(key);若返回None,再调用cache.set(key, "used", timeout=300)(注意 timeout 必须 ≤ Token 逻辑有效期)
SignatureExpired 和 BadSignature 错误怎么区分处理
Django 的 Signer 类抛出的这两个异常容易混淆,但含义完全不同:
立即学习“Python免费学习笔记(深入)”;
-
BadSignature:签名不一致 → 可能被篡改、salt 错、key 不匹配,应记日志并拒绝,**不触发重放检查** -
SignatureExpired:签名有效但超时 → 此时仍需检查是否已被使用过(防止攻击者在过期前截获并等待重放),但通常不再写入 cache(因已过期) - 实际校验顺序必须是:先解签 → 捕获
BadSignature(立即拒)→ 捕获SignatureExpired(记录+拒)→ 未异常则走 cache 重放检查
为什么不能把重放记录存进数据库而要用 cache
每笔请求都查一次 DB 会成为性能瓶颈,尤其高并发场景。更严重的是:DB 写操作不具备天然的“存在即失败”语义,需要 INSERT ... ON CONFLICT DO NOTHING(PostgreSQL)或 INSERT IGNORE(MySQL),而 Django ORM 不直接暴露这类原子操作,手写 raw SQL 易出错且破坏抽象层。
Redis 的 SET key value EX 300 NX 是原子命令:仅当 key 不存在时才设值并返回 True,否则返回 False —— 这正是防重放所需的“首次使用成功”语义。Django 的 cache.set(..., nx=True) 就是对它的封装,只要配置了 RedisCache,就自动生效。
真正复杂的是 key 设计和签名密钥轮换 —— 如果长期用同一个 settings.SECRET_KEY 签名,一旦泄露,所有历史 Token 都可伪造。建议为防重放 Token 单独配置 REPLAY_SIGNING_KEY,并定期更新。


















