本文目录导读:

在PHP项目中验证支付异步通知的真伪,核心方法是使用签名验证,以下是主流支付平台(支付宝、微信)的通用校验逻辑和代码示例。
通用校验流程
- 接收通知数据:获取POST请求中的参数
- 去除签名相关字段:去掉
sign和sign_type字段 - 按规则排序并拼接:将剩余参数按key排序后拼接成字符串
- 拼接密钥:在字符串后拼接商户密钥
- 生成签名:使用MD5或SHA256等算法计算签名
- 比对签名:比对生成的签名与支付平台传来的
sign值
支付宝异步通知校验
1 核心校验代码
<?php
/**
* 支付宝异步通知签名验证
* @param array $data 支付宝异步通知的所有参数
* @param string $alipayPublicKey 支付宝公钥
* @return bool
*/
function verifyAliPayNotify($data, $alipayPublicKey) {
// 1. 验证通知ID(可选,强烈建议)
if (!verifyNotifyId($data['notify_id'])) {
return false;
}
// 2. 签名验证
$sign = $data['sign'];
$signType = $data['sign_type'] ?? 'RSA2';
// 移除签名相关字段
unset($data['sign'], $data['sign_type']);
// 按key排序
ksort($data);
// 拼接参数
$toSign = '';
foreach ($data as $key => $value) {
if (!empty($value) && $value !== 'null') {
$toSign .= $key . '=' . $value . '&';
}
}
$toSign = rtrim($toSign, '&');
// 使用OpenSSL验证签名
$key = "-----BEGIN PUBLIC KEY-----\n" .
wordwrap($alipayPublicKey, 64, "\n", true) .
"\n-----END PUBLIC KEY-----";
$result = openssl_verify(
$toSign,
base64_decode($sign),
$key,
$signType === 'RSA2' ? OPENSSL_ALGO_SHA256 : OPENSSL_ALGO_SHA1
);
return $result === 1;
}
/**
* 验证notify_id(官方API验证通知是否合法)
*/
function verifyNotifyId($notifyId) {
// 调用支付宝验证接口
$apiUrl = 'https://mapi.alipay.com/gateway.do?service=notify_verify&partner=YOUR_PARTNER_ID¬ify_id=' . $notifyId;
$response = file_get_contents($apiUrl);
return $response === 'true';
}
2 完整处理流程
// 收到支付宝异步通知
$alipayData = $_POST;
$alipayPublicKey = '你的支付宝公钥';
// 1. 业务验证
if ($_POST['trade_status'] == 'TRADE_SUCCESS') {
// 2. 签名验证
if (verifyAliPayNotify($alipayData, $alipayPublicKey)) {
// 3. 验证商户订单号
$orderNo = $_POST['out_trade_no'];
$totalAmount = $_POST['total_amount'];
// 4. 验证金额(与数据库中的订单金额比对)
$order = getOrderByOrderNo($orderNo);
if ($order['amount'] == $totalAmount) {
// 5. 避免重复处理(幂等性)
if ($order['status'] == 'pending') {
// 更新订单状态为已支付
updateOrderStatus($orderNo, 'paid');
// 6. 返回成功标识
echo 'success';
}
}
}
}
// 验证失败
echo 'fail';
微信支付异步通知校验
1 核心校验代码
<?php
/**
* 微信支付异步通知签名验证
* @param string $xmlData 微信返回的原始XML数据
* @param string $apiKey 商户API密钥
* @return array|bool 成功返回解析后的数组,失败返回false
*/
function verifyWxPayNotify($xmlData, $apiKey) {
// 1. 解析XML
$data = xmlToArray($xmlData);
// 2. 验证返回码
if ($data['return_code'] !== 'SUCCESS') {
return false;
}
// 3. 签名验证
$sign = $data['sign'];
unset($data['sign']);
// 按key排序
ksort($data);
// 拼接字符串
$toSign = '';
foreach ($data as $key => $value) {
if ($value !== '' && !is_array($value)) {
$toSign .= $key . '=' . $value . '&';
}
}
$toSign .= 'key=' . $apiKey;
// 生成签名(MD5)
$generatedSign = strtoupper(md5($toSign));
// 比对签名
if ($sign !== $generatedSign) {
return false;
}
// 4. 验证业务结果
if ($data['result_code'] !== 'SUCCESS') {
return false;
}
return $data;
}
/**
* XML转数组
*/
function xmlToArray($xml) {
// 禁止外部实体加载,防止XXE攻击
libxml_disable_entity_loader(true);
return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
}
2 完整处理流程
// 接收微信通知的原始数据
$xml = file_get_contents('php://input');
$apiKey = '你的商户API密钥';
// 验证签名并获取数据
$notifyData = verifyWxPayNotify($xml, $apiKey);
if ($notifyData !== false) {
// 获取订单信息
$orderNo = $notifyData['out_trade_no'];
$transactionId = $notifyData['transaction_id'];
$totalFee = $notifyData['total_fee']; // 单位:分
// 验证订单金额
$order = getOrderByOrderNo($orderNo);
if ($order['amount'] * 100 == $totalFee) { // 元转分
// 避免重复处理
if ($order['status'] == 'pending') {
updateOrderStatus($orderNo, 'paid');
// 返回成功响应
$response = array(
'return_code' => 'SUCCESS',
'return_msg' => 'OK'
);
}
}
} else {
// 验证失败
$response = array(
'return_code' => 'FAIL',
'return_msg' => '签名验证失败'
);
}
// 输出XML响应
header('Content-Type: text/xml');
echo arrayToXml($response);
安全注意事项
1 必须做的基础验证
// 1. IP白名单(可选但推荐)
$validIps = ['119.29.29.29', '121.43.18.1']; // 支付平台IP段
if (!in_array($_SERVER['REMOTE_ADDR'], $validIps)) {
// 记录日志并拒绝
}
// 2. 订单状态幂等性检查
$order = getOrder($orderNo);
if ($order['status'] !== 'pending') {
// 已处理过,直接返回success
echo 'success';
exit;
}
// 3. 金额比对
if (abs($actualAmount - $expectedAmount) > 0.01) { // 允许极小误差
// 金额异常,记录日志
}
2 常见攻击防范
// 1. 防止重放攻击
if (isset($_POST['notify_time'])) {
$notifyTime = strtotime($_POST['notify_time']);
if (time() - $notifyTime > 3600) { // 超过1小时的通知拒绝
die('fail');
}
}
// 2. 验证通知类型
if ($_POST['notify_type'] !== 'trade_status_sync') {
die('fail');
}
// 3. 验证app_id
if ($_POST['app_id'] !== '你的APPID') {
die('fail');
}
完整示例封装
<?php
class PaymentNotifyService {
private $config;
public function __construct($config) {
$this->config = $config;
}
/**
* 处理支付宝异步通知
*/
public function handleAlipayNotify() {
$data = $_POST;
// 1. 基础验证
if (!$this->basicCheck($data)) {
return 'fail';
}
// 2. 签名验证
if (!$this->verifyAlipaySign($data)) {
logError('支付宝签名验证失败', $data);
return 'fail';
}
// 3. 业务处理
return $this->processOrder($data);
}
/**
* 处理微信异步通知
*/
public function handleWxpayNotify() {
$xml = file_get_contents('php://input');
// 1. 解析和验证签名
$data = $this->verifyWxpaySign($xml);
if (!$data) {
return $this->xmlResponse('FAIL', '签名验证失败');
}
// 2. 业务处理
$result = $this->processWxOrder($data);
return $this->xmlResponse(
$result ? 'SUCCESS' : 'FAIL',
$result ? 'OK' : '处理失败'
);
}
private function basicCheck($data) {
// 检查必要字段
$required = ['out_trade_no', 'trade_no', 'total_amount'];
foreach ($required as $field) {
if (!isset($data[$field])) {
return false;
}
}
return true;
}
private function processOrder($data) {
$orderNo = $data['out_trade_no'];
// 加锁防止并发
$lock = new RedisLock('order_' . $orderNo);
if (!$lock->acquire()) {
return 'fail';
}
try {
// 业务逻辑...
$order = OrderModel::findByNo($orderNo);
if ($order && $order->status == 'pending') {
$order->status = 'paid';
$order->save();
}
return 'success';
} finally {
$lock->release();
}
}
}
- 核心原则:永远不要信任客户端传来的数据,所有验证基于签名和服务器端逻辑
- 签名验证:使用官方提供的SDK或按照文档严格实现签名算法
- 双重验证:签名验证 + 业务验证(金额、订单状态等)
- 幂等性:确保同一通知只处理一次
- 日志记录:详细记录所有通知数据和处理结果,便于排查问题
建议使用各支付平台官方提供的SDK,它们已经封装好了签名验证逻辑,能减少很多潜在问题。