ThinkPHP API签名验证须在中间件中实现,且必须注册在跨域中间件之后、业务中间件之前;需用getInput()获取原始参数并统一解析,严格按ksort+rawurlencode拼接签名原文,结合timestamp与nonce校验防重放。

ThinkPHP API 接口签名验证,核心目标是防篡改、防重放、防刷接口,必须在请求进入业务逻辑前完成——也就是放在中间件里,且位置很关键。
中间件注册位置不能错
签名校验中间件必须注册在 app/middleware.php 的全局中间件数组中,并满足两个硬性顺序:
- 排在
\think\middleware\AllowCrossDomain::class之后(否则跨域头可能被过滤,导致签名头丢失); - 排在所有业务控制器和日志、验证等中间件之前(否则非法请求已打日志、查库、发短信才被拦下,失去防护意义)。
注册写法必须带完整命名空间,开头有反斜杠:[\app\middleware\CheckSign::class]
写成 'app\middleware\CheckSign' 或漏掉 \,类根本不会加载。
统一读取原始参数,避免解码不一致
90% 的验签失败,是因为客户端和服务器拼的字符串不一致。关键在于:客户端对原始字符串签名,而 $request->param() 是自动 urldecode() 后的结果,空格变 +、中文乱码、BOM 字符都会破坏签名。
- 一律用
$request->getInput()获取原始请求体(含 JSON 和表单); - 根据
Content-Type分流解析:application/json就json_decode($input, true),其余用parse_str($input, $body); - 显式合并 GET 参数:
array_merge($request->get(), $parsed_body),防止POST /api/user?id=1漏掉id; - 剔除签名无关字段前先判断存在:
if (isset($params['sign'])) unset($params['sign']);,避免 PHP 警告干扰流程。
签名原文构造要严格对齐
服务端必须和客户端用完全相同的规则生成签名原文:
立即学习“PHP免费学习笔记(深入)”;
- 对参数数组执行
ksort($params, SORT_STRING),按键名 ASCII 升序排列; - 遍历拼接
key=value,每个value单独rawurlencode()(不是urlencode()); - 拼成
k1=v1&k2=v2格式,末尾不加&; - 追加固定字段:
×tamp=xxx&nonce=yyy&appid=abc(注意这些也需rawurlencode()); - 最后拼上密钥:
$str = $path . $sorted_params_str . $secret,再用hash_hmac('sha256', $str, $secret)计算。
时间戳与 nonce 防重放必须落地
光有签名不够,还得防“旧包重放”:
- 时间戳校验用
abs($client_ts - time()) ,允许 5 分钟偏差,别直接比毫秒级; - nonce 存 Redis,key 为
sign:nonce:{$appid}:{$nonce},写入必须原子操作:SET key 1 EX 600(不能分set+expire,高并发会失效); - 签名头从
X-Signature或sign字段取,比对必须用hash_equals(),防止时序攻击。



















