PAYATHON 2026

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。

备注:内容仅供参考。