alipay.trade.wap.pay 同步、异步验签均失败是什么原因?
结论
同步通知和异步通知都验签失败,连官方 PHP demo 也无法通过时,问题通常出在以下环节:
- 验签公钥错误:通知验签要使用“支付宝公钥”,不能使用应用公钥、应用私钥或支付宝根证书。
- 应用、环境和密钥不匹配:例如混用了沙箱与生产环境,或支付宝后台已经更换应用公钥,本地仍保留旧密钥。
- 待验签参数被修改:框架、中间件、URL 解码、字符集转换或业务代码改变了参数值。
- 验签字符串拼接错误:没有排除
sign和sign_type,或排序、空值处理有误。 - 签名类型不一致:通知中的
sign_type是RSA2,代码却使用RSA验签。 - 证书模式配置错误:误把应用公钥证书或支付宝根证书当成支付宝公钥证书。
如果同步和异步通知都失败,应先检查支付宝公钥和应用环境,不必先从 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 或非成功响应,让支付宝按照通知机制重试。
快速定位出错环节
可以使用同一份通知参数做三组对比:
- 使用当前线上配置的支付宝公钥验签;
- 从当前
app_id对应的开放平台配置中重新获取支付宝公钥,再次验签; - 逐项比较应用收到的参数和代理层记录的原始参数。
通常可以根据结果这样判断:
- 换成当前应用的支付宝公钥后通过,说明原公钥配置错误或已经过期;
- 同一份参数在官方 demo 中也失败,应先检查公钥、环境、
sign_type和参数完整性; - 原始入口参数可以通过,框架控制器中的参数失败,说明框架或中间件修改了参数;
- 异步通知可以通过,同步通知失败,应重点检查查询字符串是否被重复解码,以及浏览器跳转链路;
- 同步和异步通知都失败,应优先检查支付宝公钥、应用环境和证书接入方式。
最常见的原因仍然是:把应用公钥当成支付宝公钥,或使用了不属于当前 app_id 和当前环境的支付宝公钥。
备注:内容仅供参考。
