本文目录导读:

在 PHP 中处理支付宝异步通知(Asynchronous Notification)的验签,是接入支付宝支付后最关键的环节,只有验签通过,才能确认该通知是支付宝官方发送的,防止伪造。
以下是使用官方 SDK 和原生代码两种最常用的验签实现方法。
核心概念
支付宝异步通知使用的是 RSA2(SHA256withRSA) 签名算法(推荐)或 RSA。 验签需要用到支付宝公钥(是支付宝平台上的公钥,不是应用私钥)。
流程梳理:
- 接收支付宝 POST 过来的
$_POST数据。 - 取出
sign和sign_type字段。 - 将剩余参数(去除
sign、sign_type,且空值不参与)按照字典序排序,拼接成字符串。 - 使用支付宝公钥,通过 RSA2 算法验证签名是否一致。
使用官方 SDK(推荐,最简单)
支付宝官方提供了 alipay-sdk-php,如果项目已通过 Composer 安装,直接调用即可。
安装(如果还没装):
composer require alipay/alipay-sdk-php
异步通知处理代码:
<?php
// 引入自动加载文件
require_once 'vendor/autoload.php';
// 引入相关类(根据 SDK 版本不同,命名空间可能略有差异)
use Alipay\EasySDK\Kernel\Factory;
use Alipay\EasySDK\Kernel\Util\ResponseChecker;
use Alipay\EasySDK\Kernel\Config;
// 1. 获取支付宝 POST 过来的原始数据
$data = $_POST;
// 2. 初始化配置(填入你的配置)
$options = new Config();
$options->protocol = 'https';
$options->gatewayHost = 'openapi.alipay.com';
$options->signType = 'RSA2'; // 签名类型
$options->appId = '你的APPID';
$options->merchantPrivateKey = '你的应用私钥(原始字符串,不要PEM头)'; // 注意格式
// 关键:这里是支付宝公钥(不是应用公钥)
$options->alipayPublicKey = '你的支付宝公钥(原始字符串,不要PEM头)';
Factory::setOptions($options);
try {
// 3. 验签(SDK内部封装了排序和验签逻辑)
// 注意:SDK的 verify 方法需要传入未解码的原始数组
$result = Factory::payment()->common()->verifyNotify($data);
if ($result === true) { // 验签成功
// 打印日志记录
file_put_contents('notify_log.txt', json_encode($data) . PHP_EOL, FILE_APPEND);
// --- 业务处理 ---
// 1. 检查商户订单号 out_trade_no
// 2. 检查交易状态 trade_status 是否为 TRADE_SUCCESS
// 3. 检查金额 total_amount 是否与订单一致
// 4. 检查 AppID 是否匹配
// 幂等性处理:检查该订单是否已处理,避免重复发货
// ... 你的业务逻辑 ...
// 4. 验签成功且业务处理完成,必须原样输出 "success"(不要输出其他内容)
echo 'success';
} else {
// 验签失败
echo 'fail';
// 记录日志
}
} catch (\Exception $e) {
// 异常处理
file_put_contents('notify_error.log', $e->getMessage() . PHP_EOL, FILE_APPEND);
echo 'fail';
}
原生 PHP 手动验签(无 SDK 依赖)
如果你不想引入庞大的 SDK,或者需要底层逻辑,可以手动实现。
<?php
// 手动验签函数
function verifyAlipayNotify($data, $alipayPublicKey)
{
// 1. 剔除 sign 和 sign_type
$sign = $data['sign'] ?? '';
unset($data['sign']);
unset($data['sign_type']);
// 2. 去除空值(如果值为空字符串或 null,不参与签名)
$filteredData = array_filter($data, function ($value) {
return $value !== '' && $value !== null;
});
// 3. 按照字典序排序(根据键名 ASCII 码升序排列)
ksort($filteredData);
// 4. 拼接成字符串 key1=value1&key2=value2
$signStr = urldecode(http_build_query($filteredData));
// 5. 验证签名
// 注意:支付宝公钥可以是PEM格式,也可以是一行字符串,这里需要拼接成PEM
$publicKey = "-----BEGIN PUBLIC KEY-----\n" . wordwrap($alipayPublicKey, 64, "\n", true) . "\n-----END PUBLIC KEY-----";
$res = openssl_get_publickey($publicKey);
if (!$res) {
return false; // 公钥格式错误
}
// 使用 OPENSSL_ALGO_SHA256 (RSA2)
$result = openssl_verify($signStr, base64_decode($sign), $res, OPENSSL_ALGO_SHA256);
// 释放资源
openssl_free_key($res);
return $result === 1; // 1 表示验证成功
}
// --- 使用示例 ---
$data = $_POST;
// 这里填入你的支付宝公钥(在支付宝开放平台获取)
$alipayPublicKey = '你的支付宝公钥字符串';
if (verifyAlipayNotify($data, $alipayPublicKey)) {
// 验签通过
// 处理业务逻辑...
// 注意:需要再次校验业务参数
// 1. app_id 是否是你自己的
// 2. out_trade_no 是否存在
// 3. total_amount 是否匹配
// 4. seller_id 是否匹配
echo 'success';
} else {
echo 'fail';
}
关键安全校验清单(业务层面)
验签只是第一步,为了防止海豚攻击(重放攻击)和逻辑漏洞,必须再检查以下内容:
app_id:必须等于你自己应用的 AppID。out_trade_no:必须是系统中存在的订单号。total_amount:必须与你数据库中该订单的金额完全一致(注意浮点数比较,建议使用字符串比较或bccomp)。seller_id:必须是你自己的支付宝账号对应的 ID。trade_status:只有当值为TRADE_SUCCESS时,才进行财务入账操作,其他状态(如WAIT_BUYER_PAY)忽略。
常见踩坑点(高频问题)
- 公钥填错了:在支付宝后台,你需要使用的是 “支付宝公钥”,不要填成“应用公钥”或“应用私钥”。
- 空值参与签名:
array_filter去掉空值是必须的,否则验签失败。 - 解码问题:如果使用
http_build_query,注意该函数默认会进行urlencode,但在手动拼接时需要确保没有转义特殊字符,推荐使用原生的拼接方式或urldecode包裹。 - 返回字符串:验签并处理成功后,必须输出一个独立的单词
success(不要带引号、空格或换行——有些服务器会自动加换行,通常没问题,但建议不要额外 echo 其他内容),如果输出其他内容,支付宝会认为处理失败,并在接下来的 24 小时内按一定频率重发通知(最多重发 8 次)。 - 日志记录:强烈建议将
$_POST的原始数据记录到日志文件,方便排查问题,但注意不要记录sign字段泄露签名信息(或者可以记录,问题不大,但出于安全可打码)。
推荐使用官方 SDK 进行验签,因为官方 SDK 处理了:openssl 库的兼容性、公钥格式转换、字符编码等问题,手动验签虽然在上述代码中能跑通,但遇到环境差异(如 PHP 7.x 和 8.x 的 openssl_verify 行为差异,或者中文参数编码问题)时容易踩坑。
最终操作步骤:
- 拿到
$_POST数据。 - 调用 SDK 的
verifyNotify(或手动验签)。 true,再检查订单金额/状态/AppID。- 处理完业务,输出
success。