PayPal / PHP · 2022年4月12日 7

PayPal WebHook事件异步通知

PayPal WebHook

即paypal异步通知功能,订阅了相应的事件后,当发生了订阅的事件,paypal会post一个json格式的数据到我们配置的地址上。

配置WebHook地址和订阅事件

https://developer.paypal.com/developer/applications

选择查看应用详情后拉到最下面的Webhook配置,输入通知地址和订阅事件,点击保存后,会生成一个Webhook ID(在验证Webhook信息的时候用到)。

checkout order相关的事件:https://developer.paypal.com/docs/checkout/apm/reference/subscribe-to-webhooks/

所有事件:https://developer.paypal.com/api/rest/webhooks/event-names/

需要响应200状态码表示处理成功,否则会在3天内最多发送25次。

Webhook事件

  • CHECKOUT.ORDER.APPROVED 客户授权支付,此时可收款。
  • PAYMENT.CAPTURE.COMPLETED 收款完成,即交易完成。
  • PAYMENT.CAPTURE.PENDING 交易需要审核。
  • PAYMENT.CAPTURE.DENIED 拒绝了收款。
  • PAYMENT.CAPTURE.REFUNDED 退款。
  • CUSTOMER.DISPUTE.CREATED 客户发起争议,此时也会收到RISK.DISPUTE.CREATED消息,内容一致。
  • CUSTOMER.DISPUTE.RESOLVED 争议解决。

接收并验证Webhook消息

为了保证webhook消息的真实性,我们需要对消息进行验证,避免处理假消息。

请注意系统需要判断此事件消息是否处理过,避免重复处理事件。

地址:https://developer.paypal.com/api/rest/webhooks/

需要用到的header:

PAYPAL-TRANSMISSION-ID: Http传输的唯一ID

PAYPAL-TRANSMISSION-TIME:时间

PAYPAL-CERT-URL:Public key地址

PAYPAL-TRANSMISSION-SIG:签名

PAYPAL-AUTH-ALGO:签名算法

验证方式一:调接口获取webhook详情

通过Webhook的消息ID调用接口去获取webhook消息详情

GET https://api.paypal.com/v1/notifications/webhooks-events/{EVENT_ID}

验证方式二:调用验签API

调用paypal的验签API

POST https://api.paypal.com/v1/notifications/verify-webhook-signature

参数

{
    /**
     * 签名方式,可从header信息的PAYPAL-AUTH-ALGO获取
     */
    "auth_algo": string;
    /**
     * 公钥地址,可从header信息的PAYPAL-CERT-URL获取
     */
    "cert_url": string;
    /**
     * HTTP传输ID,可从header信息的PAYPAL-TRANSMISSION-ID获取
     */
    "transmission_id": string;
    /**
     * 签名,可从header信息的PAYPAL-TRANSMISSION-SIG获取
     */
    "transmission_sig": string;
    /**
     * 时间,可从header信息的PAYPAL-TRANSMISSION-TIME获取
     */
    "transmission_time": string;
    /**
     * Webhook ID,即webhook配置的ID
     */
    "webhook_id": string;
    /**
     * webhook事件
     */
    "webhook_event": string;
}

响应

{
    /**
     * 验证结果
     * SUCCESS - 成功
     * FAILURE - 失败
     */
    "verification_status": string;
}

验证方式三:自己手动验证

验证签名,输入的字符串为:

<transmissionId>|<timeStamp>|<webhookId>|<crc32>
FieldDescription
transmissionIdThe unique ID of the HTTP transmission from the PAYPAL-TRANSMISSION-ID header.
timeStampThe date and time when the HTTP message was transmitted from the PAYPAL-TRANSMISSION-TIME header.
webhookIdThe ID of the webhook resource for the destination URL to which PayPal delivers the event notification.
crc32The Cyclic Redundancy Check (CRC32) checksum for the body of the HTTP payload.

使用crc32算法对post的数据进行多项式计算。

PHP代码
<?php
$dataUrlString = file_get_contents('php://input');
$headers = self::getAllHeaders();// 获取所有header
$paypalTransmissionId = $headers['PAYPAL-TRANSMISSION-ID'] ?? '';
$timeStamp = $headers['PAYPAL-TRANSMISSION-TIME'] ?? '';
$webhookId = 'WEBHOOK ID';
$crc32 = crc32($dataUrlString);

if (!$paypalTransmissionId || !$timeStamp || !$webhookId || !$crc32) {
    return false;
}

$sigString = "{$paypalTransmissionId}|{$timeStamp}|{$webhookId}|{$crc32}";

// 通过PAYPAL-CERT-URL头信息去拿公钥
$publicKey = openssl_pkey_get_public(self::httpCurl($headers['PAYPAL-CERT-URL']));
$details = openssl_pkey_get_details($publicKey);
$verifyResult = openssl_verify($sigString, base64_decode($headers['PAYPAL-TRANSMISSION-SIG']), $details['key'], 'SHA256');
if ($verifyResult === 1) {
    // 验证成功
} else {
    // 验证失败
}
<?php
public static function getAllHeaders(): array
{
    $headers = [];
    foreach ($_SERVER as $name => $value) {
        if (substr($name, 0, 5) == 'HTTP_') {
            $headers[str_replace(' ', '-', str_replace('_', ' ', substr($name, 5)))] = $value;
        }
    }
    return $headers;
}
<?php

public static function httpCurl($url)
{
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

    $output = curl_exec($ch);
    curl_close($ch);
    return $output;
}