签名验证必须放在中间件里——控制器执行时路由已匹配、参数已解析,恶意请求可能已触发日志或业务逻辑;90%验签失败源于客户端与服务端参数编码不一致,如urldecode差异、中文编码未统一、空格/bom等隐藏字符干扰,须严格按路径+字典序参数+sign_version+密钥拼接签名原文并剔除sign字段。

签名验证必须放在中间件里,不能在控制器里做——控制器执行时路由已匹配、参数已解析,恶意请求可能已触发日志记录或业务逻辑,验签就失去第一道防线的意义。
为什么验签失败 90% 是参数没对齐
服务端 $request->param() 返回的是框架自动 urldecode() 后的结果,但客户端签名时用的是原始 URL 编码字符串(比如 a=x%20y),两者拼接后哈希值必然不同。空格、BOM、换行、隐藏 Unicode 字符也会导致不一致。
必须统一预处理:
- 所有参数值先
trim(),再str_replace(["\r", "\n"], '', $v) - 每个 value 单独用
rawurlencode()编码,不是urlencode()(后者会把空格转成+) - 中文参数前后端都确保 UTF-8,PHP 内部设
mb_internal_encoding('UTF-8') - 调试时加
debug_sign=1参数输出服务端构造的签名原文,和客户端比对
如何兼容 GET/POST/JSON 请求体并正确取参
$request->param() 对 JSON 请求体不可靠:当 Content-Type 是 application/json 但未触发自动解析时,它返回空数组,导致签名原文缺失关键字段。
统一做法是手动读原始输入流:
- 用
$request->getInput()取原始数据 - 判断
$request->header('content-type')是否含application/json - 是则用
json_decode($input, true)解析;否则用parse_str($input, $data) - 显式合并
array_merge($request->get(), $parsed_body),避免漏掉 query 参数 - 剔除
sign、sign_version(若存在)、app_id等非业务字段前,先确认它们在数组中
签名原文拼接顺序和字段过滤规则
签名原文不是“把所有参数连起来”,而是有严格顺序和过滤规则的字符串:接口路径 + 按 key 字典序排列的参数键值对 + sign_version(如有)+ 密钥,其中 sign 字段必须剔除。
实操要点:
- 路径用
$request->url(true)(不含域名,如/api/v1/user) - 参数排序必须用
ksort(),不要依赖http_build_query()自动排序 - 遍历排序后数组,对每个
$k和$v分别rawurlencode(),拼成$k=$v -
sign_version必须参与签名,但sign字段必须从待签名参数中彻底剥离 - 最后追加
rawurlencode($secretKey),别漏掉,也别多加空格或换行
中间件注册位置和密钥加载陷阱
中间件必须注册在 app/middleware.php 全局链中,且顺序很关键:要排在 AllowCrossDomain 之后、业务控制器之前;挂太早(如 TrustHosts 前)会导致原始请求头被过滤,挂太晚就失去拦截意义。
密钥加载常见错误:
- 每次验签都
file_get_contents()读 PEM 文件 → 高并发下 I/O 和解析瓶颈 - 硬编码密钥 → 泄露风险极高
- 公钥格式用错:PHP 8.0+ 不认
-----BEGIN RSA PUBLIC KEY-----(PKCS#1),必须是-----BEGIN PUBLIC KEY-----(PKCS#8) - 验签函数第三个参数写成
'sha256'字符串 → 必须传常量OPENSSL_ALGO_SHA256
密钥应缓存为 openssl_pkey_get_private() 返回的 resource 类型,用 think\Container 单例封装复用,文件路径避开 Web 可访问目录(如 runtime/keys/private.pem)。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











