防重放必须签名、时间戳、nonce三者配合且顺序严格:先验签名,再验±180秒时间戳,最后查nonce去重;redis中nonce需用原子操作set+ex设置,key为nonce:{app_id}:{nonce},ttl与时间窗口一致为300秒。

直接上结论:只校验 timestamp 不防重放,只存 nonce 不设 TTL 会爆 Redis,两者必须配合且顺序不能错——先验签名,再验时间,最后查 nonce。
ThinkPHP 中 timestamp 校验为什么总误杀合法请求
常见错误是写成 time() - $params['timestamp'] > 300,但没处理时区和 NTP 偏移。当前服务器时间(2026年4月30日)若未同步 NTP,偏差可能超 2 秒;客户端用手机系统时间生成 timestamp,误差常达 5–10 秒。
实操建议:
- 统一用
abs(time() - (int)$params['timestamp']) > 180,允许 ±3 分钟,比硬卡 60 秒更稳妥 -
timestamp必须从请求头(如X-Timestamp)读取,禁止从 URL 或 body 解析——否则 CDN 缓存后所有请求共享同一时间戳 - 别在控制器里零散写校验逻辑,抽成中间件,全局拦截
app_id、timestamp、nonce、sign四个字段是否存在
nonce 去重必须用 SETNX+EXPIRE 原子操作
直接 redis_set("nonce:{$nonce}", 1); redis_expire("nonce:{$nonce}", 300); 在高并发下会漏判:两个请求几乎同时写入,都成功设置 key,但只有第一个能设 TTL,第二个覆盖了第一个的过期时间,导致去重失效。
正确做法:
- 用
Redis::setex()(如果驱动支持)或Redis::executeRaw(['SET', "nonce:{$nonce}", '1', 'EX', 300]) - key 命名格式固定为
nonce:{$app_id}:{$nonce},避免不同应用 nonce 冲突 - TTL 设为 300 秒(5 分钟),和
timestamp容忍窗口一致,方便清理和排查 - 校验时先
redis_get("nonce:{$app_id}:{$nonce}"),命中即拒收,不命中才写入
签名验证顺序错了会导致安全形同虚设
很多项目把 hash_equals($expected, $params['sign']) 放最后,结果攻击者传一个超时的 timestamp 或重复 nonce,服务端还没走到签名校验就直接报错返回,反而暴露了接口结构和参数规则。
必须按这个顺序执行:
- 先检查
app_id是否存在、是否启用 - 再校验
sign—— 签名不过,立刻返回 401,不进任何业务逻辑 - 然后验
timestamp有效性(±180 秒) - 最后查
nonce是否已存在,不存在则立即写入 Redis
示例关键片段:
if (!hash_equals($expectedSign, $params['sign'])) {
throw new HttpException(401, 'Invalid sign');
}
if (abs(time() - (int)$params['timestamp']) > 180) {
throw new HttpException(401, 'Timestamp expired');
}
$key = 'nonce:' . $params['app_id'] . ':' . $params['nonce'];
if (Redis::get($key)) {
throw new HttpException(401, 'Nonce reused');
}
Redis::setex($key, 300, 1);
最易被忽略的一点:密钥 $secret_key 绝对不能写死在代码里,必须从环境变量(如 $_ENV['API_SECRET'])或配置中心加载,否则 Git 提交一次,整套防重放机制就等于公开源码。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











