ThinkPHP中需通过中间件统一校验请求头Sign,签名原文按路径、字典序请求头、原始body固定顺序拼接,用sha256+动态密钥计算;严格校验X-Timestamp(±180秒容差)与X-Nonce(Redis去重),并支持新旧密钥/算法双兼容。

ThinkPHP里怎么加请求头签名(Sign)校验
核心是服务端对请求头里的 Sign 做一致性验证,不是靠前端传个固定值糊弄。必须用请求体 + 时间戳 + 密钥 + 接口路径等可复现字段拼接后做哈希,否则攻击者截包改参数就能绕过。
常见错误是只对参数排序哈希,却忽略 HTTP_X_TIMESTAMP 或 HTTP_X_NONCE 这类自定义头字段;或者用 md5($_POST) 这种不可控序列化方式,导致前后端结果不一致。
- 推荐用
sha256而非md5,防碰撞更稳妥 - 签名原文必须固定顺序:先拼接接口路径(如
/api/v1/user/info),再按字典序拼所有带值的请求头(X-Timestamp=1718234567&X-Nonce=abc123...),最后追加原始 POST body(注意:空 body 也要参与,不能跳过) - 密钥不要硬编码在控制器里,走
config('app.api_secret'),且生产环境务必从环境变量读取
时间戳怎么防重放(Replay Attack)
只校验 X-Timestamp 是否存在没用,关键得判断它是否“太老”。ThinkPHP 默认不拦截超时请求,得自己加中间件或在基类控制器里统一处理。
典型坑是直接用 time() - $timestamp > 300,但没考虑服务器时区或 NTP 同步误差,导致合法请求被误拒;还有人把时间戳放 URL 里,被 CDN 缓存后彻底失效。
立即学习“PHP免费学习笔记(深入)”;
-
X-Timestamp必须由客户端生成并传入请求头,禁止从 URL 或 body 读取 - 允许误差建议设为 ±180 秒,用
abs(time() - (int)$timestamp) > 180判断 - 配合
X-Nonce(一次性的随机字符串)做去重,Redis 存nonce:xxx并设 300 秒 TTL,重复则拒绝
ThinkPHP 6/7 的中间件怎么统一拦截非法 Sign
别在每个控制器方法里写校验逻辑,容易漏、难维护。中间件才是正解,但要注意生命周期——必须在路由解析后、控制器执行前触发,否则拿不到完整请求体。
常见错误是注册成全局中间件,结果连静态资源请求都被拦;或者用 $request->param() 取 body,但 JSON 请求下会为空(得用 $request->raw())。
- 中间件类里用
$request->header('x-sign')和$request->header('x-timestamp')读头 - 获取原始 body:JSON 请求用
$request->raw(),表单用$request->input('', '', false)(第三个参数关掉自动过滤) - 校验失败直接抛
think\exception\HttpException,状态码 401,别 return 模板或 JSON - 注册时限定路径:在
app/middleware.php里配'api/*' => [SignCheck::class]
签名算法和密钥轮换怎么不影响线上业务
上线后想换密钥或升级哈希算法?别直接切,必须兼容双签名校验窗口期。否则 App 更新慢的用户会大面积报错。
最容易被忽略的是:旧版客户端可能把时间戳当字符串传(如 "1718234567"),新版校验时强转 (int) 会截断;或者签名时没 trim body 两端空白,导致 JSON 换行缩进差异引发失败。
- 新旧密钥并存时,在校验逻辑里先用新密钥试,失败再用旧密钥试,任一通过即放行
- 算法升级期间保留旧算法分支,比如同时支持
sha256和hmac_sha256,通过额外 header(如X-Sign-Version: v2)区分 - 所有签名相关字段(
X-Sign、X-Timestamp、X-Nonce)都应 trim 空格再参与计算
签名这事,细节全在字段选取、顺序、编码和时钟同步上。少一个 trim(),多一个时区配置,就可能让整个防护形同虚设。











