PAYATHON 2026

PHP 调用 alipay.trade.wap.pay 时签名校验失败

支付小周

结论

out_trade_no 应按字符串处理,但不需要手工拼接双引号。生成 biz_content 时,json_encode() 会自动为字符串加上双引号。

签名失败通常不是因为“字符串不能签名”,而是参与签名的 biz_content 与实际 POST 给支付宝的内容不完全一致。示例中的 product_code 是 QUICK_MSECURITY_PAY,适用于 alipay.trade.app.pay。调用 alipay.trade.wap.pay 时,应改为 QUICK_WAP_WAY。

如果 APP 需要调用原生支付宝支付,应使用 alipay.trade.app.pay。只有确实需要在 APP 内打开手机网站支付页面时,才使用 alipay.trade.wap.pay。

为什么数字和字符串会导致验签结果不同

下面两个 JSON 的含义和字节内容都不相同:

{"out_trade_no":1642406976}
{"out_trade_no":"1642406976"}

RSA2 签名针对完整字符串的字节序列。签名完成后,只要内容出现以下任一变化,验签就可能失败:

  • 数字变成字符串,或字符串变成数字
  • JSON 被重新编码
  • 空格、斜杠或 Unicode 转义方式发生变化
  • 参数经过 htmlspecialchars() 等 HTML 转义
  • 在签名前进行了 URL 编码
  • POST 时重新拼装了 biz_content
  • 参数首尾增加或删除了空格

因此,不能先用数字形式生成签名,再把它改成带双引号的字符串提交。正确的做法是先确定最终业务参数,只生成一次 biz_content,然后在签名和提交时复用同一个字符串。

正确的数据类型和参数值

商户订单号是标识符,不用于数值计算,应始终保存为字符串:

$outTradeNo = '1642406520';

不要手工添加 JSON 双引号:

$outTradeNo = '"1642406520"';

也不要保留无意义的首尾空格:

$outTradeNo = 'M 1642406520 ';

订单号最好只使用稳定、可控的字符,例如数字、字母、下划线和连字符:

$outTradeNo = 'M1642406520';

具体长度和字符限制以当前接口文档中的 out_trade_no 参数说明为准。

PHP 请求示例

下面的示例保留了关键处理过程。实际项目应优先使用支付宝官方 PHP SDK,由 SDK 处理排序、签名和请求参数编码。

<?php

$bizData = [
    'out_trade_no' => (string) '1642406520',
    'total_amount' => '0.01',
    'subject' => 'test_s',
    'product_code' => 'QUICK_WAP_WAY',
    'quit_url' => 'https://example.com/pay/quit',
];

// 只生成一次,后面签名和 POST 必须复用该变量。
$bizContent = json_encode(
    $bizData,
    JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);

if ($bizContent === false) {
    throw new RuntimeException('biz_content JSON 编码失败:' . json_last_error_msg());
}

$params = [
    'app_id' => '你的 app_id',
    'method' => 'alipay.trade.wap.pay',
    'format' => 'JSON',
    'charset' => 'utf-8',
    'sign_type' => 'RSA2',
    'timestamp' => date('Y-m-d H:i:s'),
    'version' => '1.0',
    'notify_url' => 'https://example.com/alipay/notify',
    'return_url' => 'https://example.com/alipay/return',
    'biz_content' => $bizContent,
];

$signContent = buildSignContent($params);
$params['sign'] = rsa2Sign($signContent, $privateKey);

// URL 编码只在签名完成后进行。
$postBody = http_build_query($params, '', '&', PHP_QUERY_RFC3986);

$ch = curl_init('https://openapi.alipay.com/gateway.do');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $postBody,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/x-www-form-urlencoded;charset=utf-8',
    ],
]);

$response = curl_exec($ch);

if ($response === false) {
    throw new RuntimeException('请求失败:' . curl_error($ch));
}

curl_close($ch);

echo $response;

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

    ksort($params);

    $parts = [];

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

        // 此处必须使用原始值,不能 urlencode 或 htmlspecialchars。
        $parts[] = $key . '=' . $value;
    }

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

function rsa2Sign(string $content, string $privateKey): string
{
    $key = openssl_pkey_get_private($privateKey);

    if ($key === false) {
        throw new RuntimeException('无法加载应用私钥');
    }

    $success = openssl_sign(
        $content,
        $signature,
        $key,
        OPENSSL_ALGO_SHA256
    );

    if (!$success) {
        throw new RuntimeException('RSA2 签名失败');
    }

    return base64_encode($signature);
}

私钥必须是 OpenSSL 能够识别的 PEM 内容,例如:

$privateKey = <<<'KEY'
-----BEGIN PRIVATE KEY-----
应用私钥内容
-----END PRIVATE KEY-----
KEY;

如果现有私钥只有 Base64 内容,需要按照密钥格式补上正确的 PEM 头尾。PRIVATE KEY 和 RSA PRIVATE KEY 两种格式不能混用。

APP 支付与 WAP 支付不要混用

这两类接口通常采用不同的组合:

使用场景APIproduct_code
APP 原生支付alipay.trade.app.payQUICK_MSECURITY_PAY
手机网站支付alipay.trade.wap.payQUICK_WAP_WAY

下面的组合存在接口类型不匹配的问题:

{
  "product_code": "QUICK_MSECURITY_PAY"
}
method=alipay.trade.wap.pay

调用 alipay.trade.wap.pay 时,应改为:

{
  "product_code": "QUICK_WAP_WAY"
}

WAP 支付通常返回供浏览器跳转或提交的页面内容。如果 APP 把响应当成普通 JSON 接口处理,也可能出现“页面出错”的现象。

排查签名时应比较什么

不要根据错误页面中的 HTML 文本反推验签字符串,也不要用 htmlspecialchars_decode() 修正请求。HTML 转义属于页面展示层,与支付宝实际参与验签的原始请求内容不是一回事。

发送请求前,可以记录以下内容:

error_log('biz_content=' . $bizContent);
error_log('sign_content=' . $signContent);
error_log('post_body=' . $postBody);

检查时重点确认:

  1. sign_content 中的 biz_content 与 POST 参数解码后的 biz_content 完全一致。
  2. 签名前没有调用 urlencode()、htmlspecialchars() 或 addslashes()。
  3. http_build_query() 只在签名完成后调用。
  4. 生成签名后没有再次执行 json_encode()。
  5. 使用应用私钥签名,并使用对应的应用公钥验签。
  6. charset、sign_type 和网关环境配置一致。
  7. 没有混用沙箱应用、正式应用及其密钥。

处理时只需遵循一条原则:先将 out_trade_no 设为普通 PHP 字符串,再生成唯一一份 biz_content。签名使用这份原始字符串,最终 POST 时也原样使用它。

备注:内容仅供参考。