必须按签名→时间戳→nonce顺序校验:先验sign,再查abs(time()-$params['timestamp'])>180,最后redis原子set nonce:{$app_id}:{$nonce}带nx和ex参数,否则易暴露接口或绕过签名。

ThinkPHP 中防重放必须按签名→时间戳→nonce顺序校验
顺序错了就等于没防:先验 sign,再查 abs(time() - $params['timestamp']) > 180,最后用 Redis 原子操作 SET 带 nx 和 ex 参数写 nonce:{$app_id}:{$nonce}。如果把 timestamp 校验放前面,攻击者传个超时时间直接触发 400 错误,反而暴露了接口结构;如果 nonce 查重放最前,没验签就允许进逻辑,签名可被绕过。
常见错误:
- 控制器里零散写校验,漏掉
app_id存在性检查 - 用
time() - $params['timestamp'] > 300判超时,没取绝对值,客户端时间快几秒就误杀 - 从 URL 或 POST body 解析
timestamp,CDN 缓存后所有请求共享同一值
Redis 存 nonce 必须用原子 SET + EX,别分两步
高并发下 SETNX 后跟 EXPIRE 是典型竞态:A 请求刚 SETNX 成功,B 请求立刻判断 key 不存在又成功 SETNX,结果两个都写入,去重失效。
正确做法(PHP + Predis):
$result = $redis->set($nonce, '1', ['nx', 'ex' => 300]);
关键点:
-
nx保证只在 key 不存在时写入 -
ex绑定 TTL,和时间窗口一致(300 秒) - key 格式固定为
nonce:{$app_id}:{$nonce},避免多应用冲突 - 不要用
INCR或GETSET替代——前者无法限制初始值,后者不天然防并发
nonce 生成和签名原文必须加密安全且绑定参数
前端用 Date.now() + Math.random() 拼 nonce 是无效的:JS 时间不准、Math.random() 可预测、移动端休眠后跳变。服务端生成也错——API 是无状态的,不能依赖 PHP session。
正确方式:
- 客户端用
random_bytes(16)生成,转成 hex:bin2hex(random_bytes(16)) - 签名原文必须包含
timestamp、nonce和所有业务参数(按字典序排序后拼接),缺一不可 - 算法统一用
hash_hmac('sha256', $signStr, $secretKey),$secretKey从配置或环境变量读,绝不硬编码 - 请求头传
X-Timestamp和X-Nonce,body 只传业务数据,避免参数污染
PHP 8.5.5 下中间件统一拦截比控制器手写更可靠
PHP 8.5.5 本身不新增防重放能力,但它的严格类型检查和错误修复让中间件逻辑更稳——比如 getRawBody() 不会因多次调用返回空,hash_equals() 的恒定时间比较更难被时序攻击利用。
实操建议:
- 在
app/middleware/CheckSign.php里集中处理:$request->getRawBody()读一次原始体,再解析为数组 -
sign字段兼容 GET/POST,从$request->param()统一取,不区分来源 - 验证失败立刻抛
HttpException(401),不走后续业务,也不返回200+ 错误码 - 校验通过后不销毁
nonce——Redis 已设 TTL,自动过期即可;但必须unset($_SESSION['csrf_token'])(若同时用 CSRF 防护)
最容易被忽略的是 timestamp 的 ± 容忍窗口和 nonce 的原子写入这两点,差一点就形同虚设。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











