PHP项目支付异步通知如何校验真伪

wen PHP项目 21

本文目录导读:

PHP项目支付异步通知如何校验真伪

  1. 通用校验流程
  2. 支付宝异步通知校验
  3. 微信支付异步通知校验
  4. 安全注意事项
  5. 完整示例封装

在PHP项目中验证支付异步通知的真伪,核心方法是使用签名验证,以下是主流支付平台(支付宝、微信)的通用校验逻辑和代码示例。

通用校验流程

  1. 接收通知数据:获取POST请求中的参数
  2. 去除签名相关字段:去掉signsign_type字段
  3. 按规则排序并拼接:将剩余参数按key排序后拼接成字符串
  4. 拼接密钥:在字符串后拼接商户密钥
  5. 生成签名:使用MD5或SHA256等算法计算签名
  6. 比对签名:比对生成的签名与支付平台传来的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&notify_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();
        }
    }
}
  1. 核心原则:永远不要信任客户端传来的数据,所有验证基于签名和服务器端逻辑
  2. 签名验证:使用官方提供的SDK或按照文档严格实现签名算法
  3. 双重验证:签名验证 + 业务验证(金额、订单状态等)
  4. 幂等性:确保同一通知只处理一次
  5. 日志记录:详细记录所有通知数据和处理结果,便于排查问题

建议使用各支付平台官方提供的SDK,它们已经封装好了签名验证逻辑,能减少很多潜在问题。

抱歉,评论功能暂时关闭!