time() + rand() 不能当 nonce,因缺乏服务端可验证的唯一性与自动过期机制;真正可用的 nonce 必须由加密安全随机源生成(如 random_bytes),配合 Redis 原子操作(SETNX + EXPIRE)校验未使用且限时失效。

为什么 time() + rand() 不能当 nonce
直接拼时间戳加随机数生成 nonce,看似有“随机性”,实则毫无防重放意义。攻击者截获一次请求后,完全可复用该 nonce 重复发送——服务端没存、没查、没标记过期,等于没校验。
真正可用的 nonce 必须满足两个硬条件:服务端可验证其未被使用过,且在合理窗口期内自动失效。常见错误还包括把 nonce 存在 PHP session 里(无状态 API 不适用)、或只校验不删除(导致一次有效多次重放)。
实操建议:
-
nonce值应为加密安全的随机字符串,用random_bytes(16)或bin2hex(random_bytes(16))生成,避免mt_rand()等可预测源 - 必须搭配
timestamp参数一起传,且服务端严格校验时间差(如 ±5 分钟),防止拖长有效期 - 存储
nonce要选低延迟、支持原子操作的介质,如 Redis 的SETNX+EXPIRE组合,而非 MySQL 普通表(并发高时易漏判)
Redis 实现 nonce 一次性校验的最小可行代码
核心是利用 Redis 的原子性:写入成功即代表首次使用,同时设置过期时间。不能分两步先 GET 再 SET,否则竞态下会失效。
立即学习“PHP免费学习笔记(深入)”;
示例逻辑(PHP + Predis):
$redis = new Predis\Client();
$nonce = $_POST['nonce'] ?? '';
$timestamp = (int)($_POST['timestamp'] ?? 0);
<p>// 时间校验
if (abs(time() - $timestamp) > 300) {
die('{"code":401,"msg":"timestamp expired"}');
}</p><p>// 原子写入 nonce,仅当不存在时成功,且 5 分钟后自动删除
$result = $redis->set($nonce, '1', ['nx', 'ex' => 300]);
if ($result !== true) {
die('{"code":401,"msg":"nonce reused"}');
}
注意:set(..., ['nx', 'ex' => 300]) 是关键,nx 保证唯一性,ex 绑定生命周期。不要用 INCR 或 GETSET 替代——前者无法限制初始值,后者不天然防并发。
客户端怎么生成和携带 nonce 才不翻车
前端 JS 或 App 端若用 Date.now() + Math.random() 拼 nonce,会导致服务端无法稳定校验:JS 时间可能不准、Math.random() 不够随机、移动端休眠后时间跳变。
正确做法是让客户端每次请求前主动向服务端申请一个 nonce(轻量接口,无需鉴权),再拿它签名后续业务请求。或者更简单:由客户端生成高强度 nonce(如 Web Crypto API 的 crypto.randomUUID() 或 crypto.getRandomValues()),但必须同步传准确 timestamp(用服务端返回的系统时间,而非本地)。
必做检查:
- 所有请求必须带
timestamp和nonce两个字段,缺一不可 -
nonce字段值禁止 URL 编码后二次解码(如%2B变成+导致值变更),统一用原始字符串比对 - 若走 HTTPS,确保反向代理(如 Nginx)没缓存含
nonce的响应,否则可能把“已用”状态透给下一个用户
和签名、Token 混用时容易忽略的细节
很多人把 nonce 和 JWT 的 jti、或 Hmac 签名里的时间戳混为一谈。其实 nonce 是独立校验层:签名保证参数不被篡改,nonce 保证请求不被重放,二者缺一不可。
典型坑点:
- 在 JWT 验证通过后才校验
nonce—— 错。应在校验签名前就完成nonce检查,否则攻击者可反复提交无效签名+有效nonce消耗服务端资源 - 把
nonce存进数据库并手动删记录 —— 慢且易出错。Redis 的自动过期 + 原子写入才是正解 - 同一用户多个设备共用一个 Token 时,
nonce校验不区分设备或会话 ID,导致合法用户双开应用失败。此时需将noncekey 设计为"nonce:{user_id}:{device_id}:{nonce}",而非单纯"nonce:{nonce}"
最麻烦的其实是测试环节:本地开发用 file-cache 模拟 Redis 容易忽略原子性,压测时单机 Redis 若没开 AOF 或持久化策略,崩溃后 nonce 状态丢失,可能误放行重放请求——生产环境务必用真实 Redis 集群并开启 appendonly yes。



















