PHP验证Paypal Webhook签名

12

我正在尝试使用PHP验证Paypal Webhook签名,但遇到了问题。使用Paypal新的API V2版本,我在我的页面上接收到了Paypal Webhook,但似乎无法成功验证签名。

这里获得了一些来自Paypal的示例Webhook验证PHP代码。

我无法使其工作,不知道我应该从Paypal代码中的哪里获取bootstrap.php文件。Paypal提供的信息似乎不完整或者不够完善。相比之下,设置Stripe要简单得多。

是否有人在使用Paypal API V2时,用PHP验证Paypal Webhook签名方面有经验?


1
展示你的代码,你目前尝试了什么? - Grumpy
@Grumpy 我已经使用了Paypal在上面链接中提供的完全相同的代码。 - mister_cool_beans
1
您可以在此处找到bootstrap.php的信息:https://github.com/paypal/PayPal-PHP-SDK/blob/master/sample/bootstrap.php。 - Grumpy
@Grumpy 那份文档似乎全都是关于PayPal API V1的内容。我正在使用V2版本。 - mister_cool_beans
对于最近试图解决这个问题的任何人,可以尝试以下方法:1)尝试使用他们的旧代码示例,2)使用验证URL和带有身份验证令牌的新文档,或3)手动验证。我个人发现,在(2)中,他们的系统不能及时响应资源ID,因此订单在后续的Webhook调用中会延迟。这是PHP的最佳解决方案(3):https://www.bahjeez.com/validating-paypal-webhooks-offline-almost/ - Tim
2个回答

25
我得出结论,Paypal开发者信息非常不好,分散在多个不同的页面和网站上。他们在这里提供的示例并不是验证Webhook签名所需的完整图片。相比之下,Stripe开发者文档的格式更好,更简洁。
安装Paypal Checkout V2 SDK并不能为您提供必要的开发工具来验证paypal webhook签名,也就是说,您可以处理付款并接收webhook,但无法验证webhook签名......很愚蠢。提示:不要直接下载SDK,因为您将不会包含所需的autoload.php文件。使用composer安装Paypal Checkout V2 SDK,以便获取autoload.php文件。
一旦您能够处理付款并从paypal接收webhook,您需要安装另一个名为Paypal Rest API SDK的SDK。同样,请使用composer安装SDK,以便获取所需的autoload.php文件。
当您安装Paypal Rest API SDK时,仍然缺少验证Payapl webhook签名所需的文件。我找不到任何地方提到这些。bootstrap.php和common.php 感谢@Grumpy在github上提供了一些示例这里 请注意,您可能需要修改示例才能使其与您的网站配合使用。提示:将记录器设置为false,并节省一些麻烦,如果您没有必要的访问权限来编写。
创建bootstrap.php和common.php文件后,您可以编写Webhook端点页面的代码,即Paypal发送Webhook的页面。我在下面包含了我的PHP代码,以验证并处理paypal webhook。提示:在下面的代码中,您需要指定Webhook ID,每个在paypal中创建的Webhook都有唯一的ID。此外,在测试时,您无法使用Webhook模拟器,因为这将导致验证失败,您可以使用沙箱账户详细信息手动进行付款,这将触发Webhook付款事件。
Paypal肯定不会让它变得容易,他们的文档比Stripe分散得多。Paypal Webhooks有时需要数分钟才能在付款后到达,这让人非常沮丧。此外,他们在paypal开发者网站上有一个Webhook模拟器,但不能用于验证签名......如果Stripe可以做到,为什么Paypal不能呢?
<?php





//get the webhook payload

$requestBody = file_get_contents('php://input');

//check if webhook payload has data
if($requestBody) {
//request body is set
} else {
//request body is not set
exit(); 
}




use \PayPal\Api\VerifyWebhookSignature;
use \PayPal\Api\WebhookEvent;

$apiContext = require __DIR__ . '/bootstrap.php';




//Receive HTTP headers that you received from PayPal webhook.

$headers = getallheaders();


//need header keys to be UPPERCASE

$headers = array_change_key_case($headers, CASE_UPPER);


/*

example header paypal signature content for webhook, these values are recieved as an array, we then need to use this data to verify the payload


CONTENT-LENGTH : 1376

CORRELATION-ID : 6db85170269e7

USER-AGENT : PayPal/AUHD-214.0-54377828

CONTENT-TYPE: application/json

PAYPAL-AUTH-ALGO : SHA256withRSA

PAYPAL-CERT-URL : https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a784-5edc0ebc

PAYPAL-AUTH-VERSION : v2

PAYPAL-TRANSMISSION-SIG : Hc2lsDedYdSjOM4/t3T/ioAVQqFPNVB/AY/EyPNlavXk5WYUfnAmt9dyEP6neAPOjFHiVkXMK+JlLODbr6dalw6i26aFQdsPXqGl38Mafuu9elPE74qgsqNferUFgHi9QFXL+UZCNYcb4mvlDePXZIIAPbB0gOuFGOdEv2uqNwTCSAa/D8aguv1/51FWb3RkytFuVwXK/XNfIEy2oJCpDs8dgtYAZeojH8qO6IAwchdSpttMods5YfNBzT7oCoxO80hncVorBtjj1zQrkoynEB9WNNN9ytepNCkT8l29fQ4Sx/WRndm/PESCqxqmRoYJoiSosxYU3bZP7QTtILDykQ==

PAYPAL-TRANSMISSION-TIME : 2020-04-05T14:40:43Z

PAYPAL-TRANSMISSION-ID : 6dec99b0-774b-11ea-b306-c3ed128f0c4b


*/


//if any of the relevant paypal signature headers are not set exit()

if(
(!array_key_exists('PAYPAL-AUTH-ALGO', $headers)) ||
(!array_key_exists('PAYPAL-TRANSMISSION-ID', $headers)) ||
(!array_key_exists('PAYPAL-CERT-URL', $headers)) ||
(!array_key_exists('PAYPAL-TRANSMISSION-SIG', $headers)) ||
(!array_key_exists('PAYPAL-TRANSMISSION-TIME', $headers)) 
)
{

exit();     
}

//specify the ID for the webhook that you have set up on the paypal developer website, each web hook that you create has a unique ID


$webhookID = "ENTER_YOUR_WEBHOOK_ID_HERE";




//start paypal webhook signature validation 

$signatureVerification = new VerifyWebhookSignature();
$signatureVerification->setAuthAlgo($headers['PAYPAL-AUTH-ALGO']);
$signatureVerification->setTransmissionId($headers['PAYPAL-TRANSMISSION-ID']);
$signatureVerification->setCertUrl($headers['PAYPAL-CERT-URL']);
$signatureVerification->setWebhookId($webhookID); 
$signatureVerification->setTransmissionSig($headers['PAYPAL-TRANSMISSION-SIG']);
$signatureVerification->setTransmissionTime($headers['PAYPAL-TRANSMISSION-TIME']);

$signatureVerification->setRequestBody($requestBody);
$request = clone $signatureVerification;

try {

$output = $signatureVerification->post($apiContext);

} catch (Exception $ex) {

//error during signature validation, capture error and exit

ResultPrinter::printError("Validate Received Webhook Event", "WebhookEvent", null, $request->toJSON(), $ex);
exit(1);

}


$sigVerificationResult = $output->getVerificationStatus();

// $sigVerificationResult is a string and will either be "SUCCESS" or "FAILURE"


//if not webhook signature failed validation exit
if($sigVerificationResult != "SUCCESS"){

exit(); 
}
else if($sigVerificationResult == "SUCCESS"){

//paypay webhook signature is valid

//proceed to process webhook payload


//decode raw request body

$requestBodyDecode = json_decode($requestBody);


//pull whatever info required from decoded request body, some examples below


$paymentSystemID = $requestBodyDecode->id;


$eventType = $requestBodyDecode->event_type;


//do something with info captured from the webhook payload


} 

您能在本地验证此问题吗?而不是将其发送到Paypal服务器? - j10
3
根据Paypal开发文档所述,“PHP目前不支持证书链验证,而这是直接验证接收到的Webhook所必需的。为了解决这个问题,我们需要使用一种替代方法,通过调用PayPal的verify-webhook-signature API来进行验证。” 参考链接:https://paypal.github.io/PayPal-PHP-SDK/sample/doc/notifications/ValidateWebhookEvent.html - mister_cool_beans
3
这个解决方案在今天(2022年1月)仍然有效吗?好奇为什么https://github.com/paypal/PayPal-PHP-SDK上说这个软件包已被弃用(并存档)? - gvanto
3
"PayPal的开发者信息相对较少,而且分散在各个地方。如果我能给你100分的话,我一定会这么做。" - jim smith

1

这似乎适用于php 8.2:

if (openssl_verify(
        data: implode(separator: '|', array: [
            $httpPayPalTransmissionId,
            $httpPayPalTransmissionTime,
            $webhookID,
            crc32(string: $rawRequestBody),
        ]),
        signature: base64_decode(string: $httpPayPalTransmissionSignature),
        public_key: openssl_pkey_get_public(public_key: file_get_contents(filename: $httpPayPalCertUrl)),
        algorithm: 'sha256WithRSAEncryption'
    ) === 1) {
    die('OK');
} else {
    die('FAILED');
}

这对我有用,很棒的解决方案!另外,应该注意到$cachedHttpPayPalCertUrl对应于接收到的标头中的PAYPAL-CERT-URL,其中包含他们的证书URL。 - Bruno Leveque
这非常有帮助!对于未来的读者,这里存在一个安全问题。由于PHP不验证证书链,因此其他人可以替换其标头中的证书,以便您加载和验证错误的证书。基本上,请检查证书网址是否来自“paypal.com”。 - noone392
如何跟踪潜在的算法变更?考虑到PayPal在提供示例和文档方面并不真正关心开发人员,这让我相信他们未来可能会没有提前适当通知就进行更改。 - Martin Braun

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接