PAYATHON 2026

php easySDK 异步回调验签返回 false,如何排查原因?

支付阿杰

结论

verifyNotify() 返回 false,通常说明签名与当前拼接出的待验签字符串不一致。EasySDK 的这个接口一般只返回布尔值,不会指出具体是哪个参数出了问题。

排查时,重点比对以下三项:

  1. 传入验签方法的原始回调参数;
  2. SDK 最终拼接出的待验签字符串;
  3. 验签时实际使用的支付宝公钥证书。

支付下单成功,只能说明应用私钥等下单配置基本可用,不能证明异步通知所用的支付宝公钥证书或回调参数处理过程没有问题。

先确认验签参数来源

直接使用支付宝通过 application/x-www-form-urlencoded 提交的 POST 参数:

use Alipay\EasySDK\Kernel\Factory;

$parameters = $_POST;
$result = Factory::payment()
    ->common()
    ->verifyNotify($parameters);

if ($result === true) {
    // 验签成功,再处理业务
    echo 'success';
} else {
    // 验签失败
    echo 'failure';
}

不要先把参数转成 JSON,也不要对整个参数数组再次执行 urlencode()、urldecode() 或 htmlspecialchars() 等操作。

下面这些写法都可能改动原始参数:

// 不建议:JSON 转换可能改变值的表现形式
$parameters = json_decode(json_encode($_POST), true);

// 不建议:对已经解析过的参数再次解码
$parameters = array_map('urldecode', $_POST);

// 不建议:HTML 转义会改变部分字符
$parameters = array_map('htmlspecialchars', $_POST);

如果回调经过网关、框架中间件或消息队列,还要确认参数没有被重新编码、过滤或转换类型。

记录真正参与验签的数据

日志中不要只记录“返回 false”,还应保存参数名、参数值和签名字段。建议使用结构化 JSON,同时避免写入不必要的用户隐私信息:

$parameters = $_POST;

error_log(json_encode([
    'sign_type' => $parameters['sign_type'] ?? null,
    'sign_length' => isset($parameters['sign'])
        ? strlen($parameters['sign'])
        : 0,
    'parameter_keys' => array_keys($parameters),
    'parameters' => $parameters,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

主要检查:

  • sign 是否存在且内容完整;
  • sign_type 是否为预期值,常见值是 RSA2;
  • out_trade_no、trade_no、total_amount 等字段是否与实际通知一致;
  • subject、body 等字段里的空格、+、&、中文和换行是否被修改;
  • 是否混入了框架自行添加的字段;
  • 是否遗漏了支付宝通知中的字段。

调试日志可能包含交易信息,正式上线后应做脱敏处理,并设置合理的保留时间。

独立执行一次 OpenSSL 验签

可以绕开 EasySDK 的布尔封装,直接使用 OpenSSL,判断问题属于“签名不匹配”还是“证书或算法调用错误”。

下面的示例用于排查问题。待验签字符串的拼接规则必须与当前 EasySDK 版本一致。如果结果与 EasySDK 不同,应查看当前安装版本中 verifyNotify() 实际使用的拼接实现。

<?php

$parameters = $_POST;

$sign = $parameters['sign'] ?? '';
$signType = strtoupper($parameters['sign_type'] ?? 'RSA2');

unset($parameters['sign'], $parameters['sign_type']);

// 按参数名排序
ksort($parameters);

// 通知参数通常都是字符串。跳过空值,不能对值再次 URL 编解码。
$pairs = [];

foreach ($parameters as $key => $value) {
    if ($value === null || $value === '') {
        continue;
    }

    if (is_array($value) || is_object($value)) {
        throw new RuntimeException("Unexpected callback parameter: {$key}");
    }

    $pairs[] = $key . '=' . $value;
}

$content = implode('&', $pairs);

$certPath = '/absolute/path/to/alipayCertPublicKey_RSA2.crt';
$certificate = file_get_contents($certPath);

if ($certificate === false) {
    throw new RuntimeException('Cannot read Alipay public certificate');
}

$publicKey = openssl_pkey_get_public($certificate);

if ($publicKey === false) {
    throw new RuntimeException(
        'Cannot load public key: ' . collectOpenSslErrors()
    );
}

$algorithm = match ($signType) {
    'RSA2' => OPENSSL_ALGO_SHA256,
    'RSA'  => OPENSSL_ALGO_SHA1,
    default => throw new RuntimeException(
        'Unsupported sign_type: ' . $signType
    ),
};

$decodedSign = base64_decode($sign, true);

if ($decodedSign === false) {
    throw new RuntimeException('sign is not valid Base64');
}

$verifyResult = openssl_verify(
    $content,
    $decodedSign,
    $publicKey,
    $algorithm
);

error_log(json_encode([
    'sign_type' => $signType,
    'content' => $content,
    'openssl_result' => $verifyResult,
    'openssl_errors' => collectOpenSslErrors(),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

function collectOpenSslErrors(): string
{
    $errors = [];

    while (($error = openssl_error_string()) !== false) {
        $errors[] = $error;
    }

    return implode(' | ', $errors);
}

openssl_verify() 的返回值含义如下:

  • 1:签名有效;
  • 0:签名无效,通常说明待验签内容或公钥不一致;
  • -1 或 false:OpenSSL 调用出错,需要查看错误队列。

当返回 0 时,OpenSSL 通常无法指出具体哪个参数有误。数字签名只能判断整体内容是否匹配,所以仍要比对待验签字符串和原始通知。

证书模式下重点检查公钥证书

证书模式通常会涉及不同用途的证书和密钥,例如:

  • 应用公钥证书;
  • 应用私钥;
  • 支付宝公钥证书;
  • 支付宝根证书。

异步通知验签使用支付宝公钥,不是应用公钥。需要检查 EasySDK 配置中的支付宝公钥证书路径,确认它指向当前应用、当前环境对应的证书。

尤其要留意这些情况:

  • 沙箱环境与正式环境的证书混用;
  • 更换或重新生成证书后,服务器仍在使用旧文件;
  • 相对路径在 Web 进程中解析到了其他目录;
  • PHP-FPM 用户没有证书文件的读取权限;
  • 容器或多台服务器上的证书版本不一致;
  • 配置缓存或常驻进程仍保留旧配置;
  • 把应用公钥证书误当成支付宝公钥证书。

可以先检查证书文件是否可读:

$certPath = '/absolute/path/to/alipayCertPublicKey_RSA2.crt';

var_dump([
    'exists' => is_file($certPath),
    'readable' => is_readable($certPath),
    'size' => is_file($certPath) ? filesize($certPath) : null,
]);

正式环境中不要把证书全文或私钥写入日志。如果需要比较不同服务器上的证书,可以记录证书文件的 SHA-256 摘要:

error_log(hash_file('sha256', $certPath));

最常见的参数问题

+ 被错误处理为空格

表单解析会把 + 作为空格处理。如果业务框架已经解析过一次,之后又对参数值执行 urldecode(),参数就可能被二次修改。

因此,应该直接使用 PHP 已解析好的 $_POST,不要再对整个参数集解码。

使用 php://input 时解析方式不一致

如果必须读取原始请求体,应保留原文,并且只解析一次:

$rawBody = file_get_contents('php://input');

$parameters = [];
parse_str($rawBody, $parameters);

不要同时对 $rawBody 和解析后的字段重复调用 urldecode()。

删除了看似无用的字段

验签必须使用支付宝实际发送并参与签名的参数集合。不能只保留业务需要的几个字段,也不能在验签前随意删减字段。

只有 sign 和 sign_type 不参与待验签字符串的拼接。

参数值被类型化

框架可能会把字符串转换为数字或布尔值。例如:

"100.00" // 原始字符串
100.00   // 转换后可能变成 "100"

这种变化会直接导致验签失败。验签应安排在 DTO 映射、金额计算和类型转换之前。

推荐的定位顺序

  1. 保存原始请求体 php://input,同时保存 PHP 解析后的 $_POST。
  2. 确认传给 verifyNotify() 的是未经业务处理的 $_POST。
  3. 检查 sign 和 sign_type 是否存在,内容是否被截断。
  4. 输出 EasySDK 实际生成的待验签字符串。
  5. 使用同一个字符串和同一份支付宝公钥证书调用 openssl_verify()。
  6. 检查证书路径、文件摘要和运行环境,并确认沙箱或正式环境对应无误。
  7. 如果独立 OpenSSL 验签成功,但 EasySDK 仍返回 false,再检查当前安装版本的 SDK 源码、依赖版本和初始化配置。
  8. 如果 OpenSSL 返回 0,逐字段比对收到的参数与进入验签前的参数,重点检查二次解码、空格、+、中文、金额格式和换行。

验签失败时,不要处理订单,也不要向支付宝返回 success。只有验签成功,并完成商户号、应用 ID、订单号、金额和交易状态等业务校验后,才能更新订单并返回成功响应。

备注:内容仅供参考。