PAYATHON 2026

alipay.trade.wap.pay 同步、异步验签均失败是什么原因?

支付老李

结论

同步通知和异步通知都验签失败,连官方 PHP demo 也无法通过时,问题通常出在以下环节:

  1. 验签公钥错误:通知验签要使用“支付宝公钥”,不能使用应用公钥、应用私钥或支付宝根证书。
  2. 应用、环境和密钥不匹配:例如混用了沙箱与生产环境,或支付宝后台已经更换应用公钥,本地仍保留旧密钥。
  3. 待验签参数被修改:框架、中间件、URL 解码、字符集转换或业务代码改变了参数值。
  4. 验签字符串拼接错误:没有排除 sign 和 sign_type,或排序、空值处理有误。
  5. 签名类型不一致:通知中的 sign_type 是 RSA2,代码却使用 RSA 验签。
  6. 证书模式配置错误:误把应用公钥证书或支付宝根证书当成支付宝公钥证书。

如果同步和异步通知都失败,应先检查支付宝公钥和应用环境,不必先从 return_url 或 notify_url 入手。

先分清三类密钥

支付宝接口涉及三类容易混淆的密钥:

  • 应用私钥:由商户保存,用来为请求签名。
  • 应用公钥:上传到支付宝开放平台,供支付宝验证商户请求。
  • 支付宝公钥:由支付宝提供,商户用它验证同步和异步通知。

通知签名和验签的关系如下:

支付宝使用支付宝私钥签名
商户使用支付宝公钥验签

如果验签代码配置了应用公钥,即便密钥格式完全正确,验签也无法通过。

使用公钥模式时,要从当前应用对应的开放平台配置中获取支付宝公钥。使用证书模式时,则要使用对应的支付宝公钥证书,并通过当前 SDK 提供的证书模式接口验签。

推荐排查顺序

1. 核对应用和运行环境

先确认以下配置来自同一个应用和同一套环境:

  • app_id
  • 应用私钥
  • 上传到开放平台的应用公钥
  • 下载或查看到的支付宝公钥
  • 支付宝网关地址
  • 回调通知来源

重点排查这些情况:

  • 沙箱 app_id 配置了生产环境的支付宝公钥;
  • 生产网关使用了沙箱密钥;
  • 后台重新设置过应用公钥,本地配置却没有更新;
  • 多个应用共用配置文件,程序读取了另一个应用的支付宝公钥;
  • 部署环境变量覆盖了代码中的正确配置。

如果近期更换过密钥,最好从当前应用后台重新复制支付宝公钥,不要继续使用旧配置文件中的内容。

2. 同步通知读取 GET,异步通知读取 POST

alipay.trade.wap.pay 的同步返回通常会跳转到 return_url,参数来自查询字符串。异步通知会发送到 notify_url,参数通常来自表单 POST 数据。

// 同步返回
$params = $_GET;

// 异步通知
$params = $_POST;

同步参数不要从 $_POST 读取,异步通知也不要只读取 JSON 请求体。具体读取方式仍要以实际收到的 Content-Type 和所用 SDK 文档为准。

3. 保存进入业务处理前的原始参数

验签对象必须是支付宝实际发送的参数。验签前不要做以下处理:

  • 修改金额格式,例如把 10.00 转成 10;
  • 将字符串 "0" 转成整数 0;
  • 再次执行 urldecode();
  • 错误地把 + 转为空格;
  • 重新编码 JSON 字符串;
  • 改变转义字符或斜杠;
  • 将 UTF-8 转换为其他字符集;
  • 只选择部分“业务参数”参与验签。

排查时可以临时记录脱敏后的参数键名、参数值长度和 sign_type,同时对比框架解析前后的值,确认代理、WAF、中间件或框架是否修改了参数。生产日志不应长期保存隐私信息和完整交易数据。

4. 检查 sign 是否损坏

sign 是 Base64 字符串,可能包含 +、/ 和 =。如果某个环节错误处理了 URL 编码,+ 可能会变成空格。

可以先做如下检查:

$sign = $_POST['sign'] ?? '';

if (str_contains($sign, ' ')) {
    error_log('The sign parameter contains spaces; check URL/form decoding.');
}

不要把“将空格替换回 +”作为长期修复方案。应找到反向代理、网关或框架中重复解码参数的环节,并从源头修正。

5. 按通知携带的签名类型验签

目前常见的签名类型是 RSA2,对应 SHA-256。验签代码不能写死成与通知不一致的类型:

$signType = $_POST['sign_type'] ?? '';

if ($signType !== 'RSA2') {
    throw new RuntimeException('Unexpected sign_type: ' . $signType);
}

如果实际通知明确使用 RSA,则应采用 SHA-1。不要根据项目配置猜测签名类型,要检查本次通知携带的 sign_type。

PHP 手工验签示例

实际项目应优先使用与接入模式匹配的支付宝官方 SDK。下面的代码可用于检查公钥模式下的参数拼接和 RSA2 验签过程:

<?php

function formatPublicKey(string $publicKey): string
{
    $publicKey = str_replace(
        [
            "-----BEGIN PUBLIC KEY-----",
            "-----END PUBLIC KEY-----",
            "\r",
            "\n",
            " ",
        ],
        '',
        trim($publicKey)
    );

    return "-----BEGIN PUBLIC KEY-----\n"
        . chunk_split($publicKey, 64, "\n")
        . "-----END PUBLIC KEY-----\n";
}

function buildSignContent(array $params): string
{
    unset($params['sign'], $params['sign_type']);

    ksort($params);

    $pairs = [];

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

        // 使用框架解析后的原始字符串值,不要再次 urlencode/urldecode。
        $pairs[] = $key . '=' . $value;
    }

    return implode('&', $pairs);
}

function verifyAlipayNotification(
    array $params,
    string $alipayPublicKey
): bool {
    $sign = $params['sign'] ?? '';
    $signType = $params['sign_type'] ?? '';

    if ($sign === '' || $signType === '') {
        return false;
    }

    $decodedSign = base64_decode($sign, true);

    if ($decodedSign === false) {
        return false;
    }

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

    $content = buildSignContent($params);
    $publicKey = formatPublicKey($alipayPublicKey);

    return openssl_verify(
        $content,
        $decodedSign,
        $publicKey,
        $algorithm
    ) === 1;
}

// 异步通知使用 $_POST;同步返回通常使用 $_GET。
$params = $_POST;
$alipayPublicKey = '支付宝开放平台中当前应用对应的支付宝公钥';

if (!verifyAlipayNotification($params, $alipayPublicKey)) {
    http_response_code(400);
    exit('failure');
}

// 验签通过后,还必须校验 app_id、金额、商户订单号和交易状态。
exit('success');

这里的 $alipayPublicKey 必须填写支付宝公钥。如果使用证书模式,不能直接把证书正文放进这个变量,而应使用 SDK 提供的证书模式配置和验签功能。

验签通过后仍需进行业务校验

签名通过只能证明通知内容没有被篡改,并且来自持有对应支付宝私钥的一方,不能替代业务检查。至少要核对以下内容:

if (($params['app_id'] ?? '') !== $expectedAppId) {
    exit('failure');
}

if (($params['out_trade_no'] ?? '') !== $expectedOrderNo) {
    exit('failure');
}

// 金额建议使用十进制定点计算或以“分”为单位比较,避免浮点误差。
if (($params['total_amount'] ?? '') !== $expectedAmount) {
    exit('failure');
}

还应根据业务要求检查:

  • seller_id 是否属于预期收款方;
  • trade_status 是否处于业务允许处理的状态;
  • out_trade_no 是否存在;
  • 订单是否已经处理,避免重复通知造成重复发货;
  • 通知中的金额、币种和订单记录是否一致。

异步通知只有在验签和业务处理都完成后,才能返回以下纯文本:

success

如果处理失败,应返回 failure 或非成功响应,让支付宝按照通知机制重试。

快速定位出错环节

可以使用同一份通知参数做三组对比:

  1. 使用当前线上配置的支付宝公钥验签;
  2. 从当前 app_id 对应的开放平台配置中重新获取支付宝公钥,再次验签;
  3. 逐项比较应用收到的参数和代理层记录的原始参数。

通常可以根据结果这样判断:

  • 换成当前应用的支付宝公钥后通过,说明原公钥配置错误或已经过期;
  • 同一份参数在官方 demo 中也失败,应先检查公钥、环境、sign_type 和参数完整性;
  • 原始入口参数可以通过,框架控制器中的参数失败,说明框架或中间件修改了参数;
  • 异步通知可以通过,同步通知失败,应重点检查查询字符串是否被重复解码,以及浏览器跳转链路;
  • 同步和异步通知都失败,应优先检查支付宝公钥、应用环境和证书接入方式。

最常见的原因仍然是:把应用公钥当成支付宝公钥,或使用了不属于当前 app_id 和当前环境的支付宝公钥。

备注:内容仅供参考。