签名验证中间件应挂在ThinkPHP的全局中间件链app/middleware.php中,位于AllowCrossDomain之后、业务控制器之前,且需避开TrustHosts等前置过滤中间件。

签名验证中间件该挂在哪一层
ThinkPHP 的 API 签名认证,必须放在 app/middleware.php 全局中间件链里,且要排在 AllowCrossDomain 之后、业务控制器之前。挂太早(比如在 TrustHosts 前)会导致原始请求头被过滤;挂太晚(比如在控制器里手动校验)就失去了中间件的统一拦截意义。
- 推荐位置:在
app/middleware.php中,把自定义签名中间件加到['app\middleware\CheckSign']数组靠前位置,但确保在跨域中间件之后 - 不要用
->except()排除登录接口——签名逻辑本身应允许免签或走独立规则,而不是靠中间件跳过 - 如果用了多应用模式,注意中间件注册需在对应应用的
middleware.php,而非全局配置
签名算法实现要避开时间戳和 nonce 的坑
常见错误是直接拿 $_SERVER['REQUEST_TIME'] 或 time() 做时间戳比对,结果因服务器时钟偏差导致频繁验签失败。nonce 缓存没设 TTL 或没清旧值,也会引发“重复请求被拒”误判。
- 时间戳容错建议:服务端接收后只接受
abs($client_ts - time()) (5 分钟),不校验毫秒级精度 - nonce 必须存 Redis,key 形如
sign:nonce:{client_id}:{nonce},TTL 设为 600 秒,写入即过期 - 签名原文拼接顺序必须固定:按参数名 ASCII 升序排列(
ksort()),再拼key1=value1&key2=value2×tamp=xxx&nonce=yyy,最后加app_secret做 HMAC-SHA256
如何让签名中间件兼容 GET/POST/JSON 请求体
ThinkPHP 默认的 $request->param() 对 JSON 请求体解析不稳定,尤其当 Content-Type 是 application/json 但没触发自动解析时,param() 会返回空数组,导致签名原文缺失关键字段。
- 统一用
$request->getInput()读原始输入流,再根据Content-Type手动解析:application/json用json_decode($input, true),其余走parse_str($input, $data) - GET 参数必须显式合并进来:
array_merge($request->get(), $parsed_body),否则带 query 的 POST 请求会漏掉签名因子 - 签名字段(
sign、timestamp、nonce、app_id)必须从原始请求中剥离后再参与签名计算,否则形成循环依赖
调试阶段必须打开的三个日志开关
线上验签失败却无日志,90% 是因为没开中间件级别日志或屏蔽了敏感字段。ThinkPHP 默认会脱敏 sign 和 app_secret,但调试时需要看到完整签名原文和计算值对比。
立即学习“PHP免费学习笔记(深入)”;
- 临时开启
Log::write()记录原始请求参数、生成的签名原文、服务端计算出的hash_hmac('sha256', $str, $secret)结果 - 在中间件里加
if (env('APP_DEBUG')) { Log::debug("sign_raw: {$str}"); },避免上线后泄露 - 捕获
think\Exception\HttpException类型异常时,记录$e->getStatusCode()和$e->getMessage(),常见如401 Unauthorized对应签名错,400 Bad Request对应参数缺失
签名最难的不是算法本身,是请求体解析的边界情况和时钟同步的容忍度设计。很多人卡在 JSON 请求体取不到参数,或者本地测试 OK、部署后因 Nginx 缓存了 Content-Type 头导致解析失败。



















