PHP调用rsaCertCheckV1验签小程序手机号密文失败
结论
验签失败一般不是 json_decode() 导致的。只要后端收到合法的 JSON,且解析过程中没有改动 response 和 sign,转换成关联数组后就可以传给 rsaCertCheckV1。
排查时主要看以下几项:
- 从原始请求体中解析 JSON,不要只读取表单参数。
- 验签前不要解密、反序列化或重新拼装
response。 - 不要对
sign执行urldecode()、空格替换或 Base64 解码。 - 传给
rsaCertCheckV1的数组应包含原始的response和sign。 - 明确传入实际使用的签名类型,通常是
RSA2。 - 使用证书模式时,应配置支付宝公钥证书,而不是应用公钥或普通公钥字符串。
正确处理 JSON 请求
请求的 Content-Type 为 application/json 时,应先读取原始请求体,再通过 json_decode() 转换为关联数组:
<?php
$rawBody = file_get_contents('php://input');
try {
$params = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
throw new RuntimeException('请求 JSON 格式不正确:' . $e->getMessage());
}
if (
!is_array($params) ||
!isset($params['response'], $params['sign']) ||
!is_string($params['response']) ||
!is_string($params['sign'])
) {
throw new InvalidArgumentException('请求中缺少有效的 response 或 sign');
}
有些 PHP 框架已经提供了 JSON 请求体解析接口,也可以直接使用。无论采用哪种方式,最终得到的数据都应保持如下形式:
[
'response' => '小程序返回的原始 response 字符串',
'sign' => '小程序返回的原始 sign 字符串',
]
不要将完整的 JSON 字符串直接传入 rsaCertCheckV1,也不要先把 response 解析成数组,再用解析后的内容验签。
验签示例
下面以常见的支付宝 PHP SDK 调用方式为例。不同版本的 SDK 在方法参数上可能略有差异,请以项目实际安装版本的方法定义为准。
<?php
$aop = new AopClient();
$verifyParams = [
'response' => $params['response'],
'sign' => $params['sign'],
];
$passed = $aop->rsaCertCheckV1(
$verifyParams,
$alipayPublicCertPath,
'RSA2'
);
if (!$passed) {
throw new RuntimeException('支付宝签名验证失败');
}
// 验签成功后,再对 response 进行解密或业务解析
如果小程序还返回了文档规定需要参与验签的其他字段,也要保持字段名和字段值不变,并一同传入。不要参照支付回调的字段列表自行添加参数,也不要混入本地生成的业务字段。
为什么解析成数组后仍可能失败
1. 验签前修改了 response
签名对应的是请求中的原始字段值。下面这些操作都可能改变待验签内容:
$response = trim($params['response']);
$response = urldecode($params['response']);
$response = base64_decode($params['response']);
$response = json_decode($params['response'], true);
$response = stripslashes($params['response']);
即使这些操作看起来只是格式处理,处理后的内容也可能与支付宝签名时使用的内容不同。
处理顺序应该是:
读取请求体
→ json_decode
→ 使用原始 response 和 sign 验签
→ 验签成功
→ 解密或解析 response
2. sign 中的 + 被替换为空格
签名通常采用 Base64 格式,其中可能包含 +、/ 和 =。如果签名被当成 URL 表单数据重复解码,+ 可能会变成空格,导致验签失败。
JSON 解析不会把 + 转换为空格,但框架中间件、前端编码或额外调用的 urldecode() 可能造成这种变化。可以检查签名中是否出现空格,同时避免记录完整的敏感内容:
if (str_contains($params['sign'], ' ')) {
throw new RuntimeException('sign 中出现空格,可能在传输过程中被错误解码');
}
只有确认 + 确实被传输层错误替换后,才可以临时恢复并验证:
$sign = str_replace(' ', '+', $params['sign']);
不要把这段替换作为默认处理。更合适的做法是调整前端请求格式或后端中间件配置,让签名保持原样。
3. 前端重复编码
前端应直接发送小程序接口返回的字段:
fetch('/api/phone', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
response: result.response,
sign: result.sign
})
});
发送前不要执行:
encodeURIComponent(result.response);
encodeURIComponent(result.sign);
如果前端已经进行了 URL 编码,后端必须准确解码一次。不过,更建议直接取消这层编码。
4. 使用了错误的证书或签名类型
rsaCertCheckV1 用于证书模式验签,通常需要传入支付宝公钥证书的文件路径,而不是以下内容:
- 应用私钥;
- 应用公钥;
- 公钥字符串;
- 支付宝根证书;
- 另一个环境的支付宝公钥证书。
即使支付回调能够正常验签,也要确认两个请求是否来自同一环境,以及调用时使用的验签方法、证书文件和签名类型是否一致。建议显式传入 RSA2,不要依赖 SDK 的默认值。
建议的排查方式
可以分别在前端发送前和 PHP 接收后记录字段长度及摘要,不要直接输出手机号密文和完整签名:
error_log('response length=' . strlen($params['response']));
error_log('response sha256=' . hash('sha256', $params['response']));
error_log('sign length=' . strlen($params['sign']));
error_log('sign sha256=' . hash('sha256', $params['sign']));
如果前后端记录的长度或 SHA-256 摘要不一致,说明数据在传输过程或框架处理阶段发生了变化。如果两边完全一致,再检查证书文件、签名类型、SDK 版本,以及实际传入 rsaCertCheckV1 的参数范围。
示例中的 JSON 使用了弯引号 “”,而实际请求必须使用标准英文双引号:
{"response":"xxx","sign":"xxxxxx"}
如果弯引号只是问题描述中的排版效果,可以忽略。如果原始请求中确实使用了弯引号,json_decode() 将无法把它解析为合法 JSON。
备注:内容仅供参考。
