thinkphp5中间件执行顺序由app\http\middleware.php数组索引决定,签名验证必须置顶;input('param.')因自动过滤和编码导致验签失败,应改用$request->get()/post()手动合并并rawurlencode()编码后ksort排序拼接。

必须放在中间件里,不能在控制器里做——ThinkPHP5 没有内置中间件执行顺序控制,但你得手动在 app\http\middleware.php 里把签名验证中间件放到最前面,否则路由匹配、参数解析、甚至日志记录都可能先于验签执行。
为什么 input('param.') 在签名验证里是危险操作
ThinkPHP5 的 input('param.') 会自动合并 GET/POST/ROUTE 参数,并触发默认过滤(比如把空格转 +、对值做 urldecode()),而客户端签名时用的是原始请求字符串字节流。两者不一致,md5() 或 hash_hmac() 必然不等。
- 改用
$request->get()和$request->post()分开取参,再用array_merge()合并 - 手动剔除
sign、sign_version等签名相关字段,别依赖unset($param['sign'])后再排序——万一它根本不在param里呢? - 所有参数值必须过一遍
rawurlencode()(不是urlencode()),尤其含中文、空格、&、=的值 - 拼接前必须
ksort(),http_build_query()默认不保序,不能直接用
签名原文拼接规则必须严格固定
客户端和服务端只要有一处顺序或编码不一致,签名就失效。ThinkPHP5 没有自动处理路径参与签名的逻辑,得你自己加。
- 签名原文 =
$request->url(true)(不含域名的路径,如/api/user) +&+ 按 key 字典序排列的参数(k1=v1&k2=v2) +×tamp=xxx&nonce=yyy&sign_version=2+&app_secret=xxx -
sign_version必须参与拼接,且必须从$request->param('sign_version', '1')显式获取,不能靠input()自动兜底 - 时间戳校验用
abs(time() - (int)$params['timestamp']) > 300,别用$_SERVER['REQUEST_TIME']——Nginx + PHP-FPM 场景下它可能滞后 - nonce 去重必须走 Redis:
SETEX sign:nonce:{$app_id}:{$nonce} 300 1,失败直接 return 401
验签失败时怎么快速定位是哪一环崩了
ThinkPHP5 默认不打中间态日志,线上一报 Invalid signature 就只能猜。调试阶段必须临时打开关键输出,但上线前得删干净或用 env('APP_DEBUG') 包裹。
- 在中间件里加一行:
Log::debug('sign_raw', ['raw' => $rawString, 'hmac' => hash_hmac('sha256', $rawString, $secret), 'client_sign' => $clientSign]); - 不要记录完整
$rawString到生产日志(含敏感参数),但可以记md5($rawString)和substr($clientSign, 0, 8) - 如果用的是旧版 MD5 算法,注意
strtoupper(md5(...))是必须的,新版 HMAC 不需要转大写 - 密钥长度低于 32 字节时,
hash_hmac('sha256', ...)实际安全性会下降,建议用random_bytes(32)生成
最常被忽略的是:签名原文里漏了接口路径、sign_version 类型被 PHP 自动转成 int、Redis nonce key 过期时间设太短导致合法重试被拒——这些点不逐个核对,光调算法没用。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











