PayPal WebHook 事件异步通知
另一个文章 https://www.bahjeez.com/validating-paypal-webhooks-offline-almost/ ,阐述了如何实现
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天内最多发送10次。
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
|transmissionId|The unique ID of the HTTP transmission from the PAYPAL-TRANSMISSION-ID header.|
|timeStamp|The date and time when the HTTP message was transmitted from the PAYPAL-TRANSMISSION-TIME header.|
|webhookId|The ID of the webhook resource for the destination URL to which PayPal delivers the event notification.|
|crc32|The [Cyclic Redundancy Check (CRC32)](https://en.wikipedia.org/wiki/Cyclic_redundancy_check) checksum for the body of the HTTP payload.|
使用crc32算法对post的数据进行多项式计算。
<?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;
}
其他说明
- IPN收款信息中含有:Payment Type, PayPal Payer Address Status, PayPal Payer Status等字段,但是webhook信息中没有这些字段。