thinkphp 6.0 的 api 签名校验必须放在中间件(如 app/middleware/checksign.php)中,注册于 app/middleware.php 且位于 allowcrossdomain 之后、业务逻辑之前;需统一读取原始请求流、按字典序拼接参数并 rawurlencode 编码、校验 timestamp(≤300秒)与 redis 存储的 nonce 防重放,签名从 x-signature header 提取并用 hash_equals 安全比对。

在 ThinkPHP 6.0 中为 API 接口添加签名校验,是为了拦截非法请求、防止参数篡改和重放攻击,避免接口被恶意刷调用或伪造数据。这一步必须在请求进入业务逻辑前完成,否则日志已打、数据库已查、资源已消耗,验签就失去意义。
创建并注册签名校验中间件
执行命令生成中间件文件:php think make:middleware CheckSign。
打开 app/middleware.php,将中间件类名加入全局中间件数组,注意必须带反斜杠且不能使用 use 语句:
【\app\middleware\CheckSign::class】 必须写在 \think\middleware\AllowCrossDomain::class 之后、业务控制器之前。
若写成 'app\middleware\CheckSign' 或漏掉开头反斜杠,中间件将完全不触发——框架加载时会尝试在当前命名空间下找类,直接报错 Class not found。
统一读取原始请求参数
签名验证失败 90% 源于参数没对齐:客户端对原始 URL 编码字符串签名,而 $request->param() 返回的是自动 urldecode() 后的结果,空格变 +、中文乱码、BOM 字符残留都会导致哈希值不一致。
第一步:调用 $request->getInput() 获取原始输入流(含 POST body 和 JSON 内容)。
第二步:根据 Content-Type 判断解析方式:
若含 application/json,用 json_decode($input, true);
否则用 parse_str($input, $body) 解析表单数据。
第三步:显式合并 GET 参数与解析后的 body:array_merge($request->get(), $parsed_body) ——否则带 query 的 POST 请求会漏掉 ?v=1.0 这类关键签名因子。
第四步:剔除签名无关字段:unset($params['sign'], $params['sign_version'], $params['app_id']);【必须先确认这些键存在再 unset,否则 PHP 警告可能干扰后续逻辑】。
构造签名原文并比对
方法一:按字典序拼接参数
① 对参数数组执行 ksort($params, SORT_STRING),确保 ASCII 升序;
② 遍历拼接 key=value,用 & 连接,**每个 value 必须单独 rawurlencode()**(不是 urlencode()),避免空格转 +;
③ 拼接完整路径(不含域名和 query string)+ 上述字符串 + timestamp=xxx&nonce=yyy&appid=abc;
④ 最后追加密钥:$str = $path . $sorted_params_str . $secret;
⑤ 计算 hash_hmac('sha256', $str, $secret) 得到期望签名。
方法二:从 Header 提取签名值
必须用 $request->header('X-Signature') 获取,【绝不能用 $request->param('sign') 或 $_GET['sign'] ——它们可被业务参数覆盖,存在污染风险】。
比对时使用 hash_equals($expected, $provided),防时序攻击;ThinkPHP 6.0.10+ 已内置该函数,低版本需自行引入。
校验 timestamp 与 nonce
时间戳校验:
取出客户端传入的 timestamp,计算 abs($client_ts - time()),若 > 300(5 分钟),直接拒绝。
不要用 $_SERVER['REQUEST_TIME'] 替代 time(),前者是 PHP 接收请求时刻,后者是当前系统时间,偏差可能导致合法请求被拒。
Nonce 校验:
构建 Redis key:sign:nonce:{$appid}:{$nonce};
用 Redis::setNx($key, 1) 写入,并设 TTL 为 600 秒;
若返回 false,说明该 nonce 已被使用过,拒绝请求。
注意:Redis 连接必须提前初始化,若未配置或连接失败,需降级为本地缓存(仅限开发环境),线上必须依赖 Redis。
跳过白名单接口
在中间件 handle() 方法开头,先获取当前请求 URI:$uri = $request->server('REQUEST_URI')。
定义免验签路径数组:private $whitelist = ['/api/login', '/api/register', '/captcha/get'];。
用 in_array($uri, $this->whitelist) 判断,命中则直接 return $next($request)。
不要用 ->except() 方式跳过——中间件链一旦注册就全量执行,except 是路由层功能,在全局中间件中无效。











