在集成Paypal的智能按钮后,我在验证Paypal发送的webhook通知时遇到问题。我发现的例子要么已经过时,要么不起作用。
有没有办法验证webhook通知,最好是以自己动手做的方式(即,不必使用庞大复杂的Paypal API(?
据我所知,这段代码只是一个真正有效的代码。我在堆栈溢出中发现的所有其他示例都不起作用,因为在编写签名字符串时,它们没有传递webhook本身的ID,而是使用了webhook事件的ID,因此验证将失败。
一旦您在Paypal的开发者后端添加了webhook,就会生成webhook ID。创建webhook后,您将在已安装的webhook列表中看到它的id。
剩下的就很简单了:我们得到标题和HTTP正文,并使用Paypal的配方编写签名:
为了生成签名,PayPal将这些带有竖线(|(字符的项。
"这些项目";是:传输id、传输日期、webhook id和HTTP主体上的CRC。前两个可以在请求的头中找到,开发者后端的webhook id(当然,该id永远不会改变(,CRC的计算如下所示。
证书的位置也在头中,所以我们加载它并提取私钥。
最后需要注意的是:Paypal提供的算法名称(同样在标题字段中(与PHP所理解的并不完全相同。贝宝称之为";sha256WithRSA";但是CCD_ 1将期望";sha256WithRSA加密";。
// get request headers
$headers=apache_request_headers();
// get http payload
$body=file_get_contents('php://input');
// compose signature string: The third part is the ID of the webhook ITSELF(!),
// NOT the ID of the webhook event sent. You find the ID of the webhook
// in Paypal's developer backend where you have created the webhook
$data=
$headers['Paypal-Transmission-Id'].'|'.
$headers['Paypal-Transmission-Time'].'|'.
'[THE_ID_OF_THE_WEBHOOK_ACCORDING_TO_DEVELOPER_BACKEND]'.'|'.
crc32($body);
// load certificate and extract public key
$pubKey=openssl_pkey_get_public(file_get_contents($headers['Paypal-Cert-Url']));
$key=openssl_pkey_get_details($pubKey)['key'];
// verify data against provided signature
$result=openssl_verify(
$data,
base64_decode($headers['Paypal-Transmission-Sig']),
$key,
'sha256WithRSAEncryption'
);
if ($result==1) {
// webhook notification is verified
...
}
elseif ($result==0) {
// webhook notification is NOT verified
...
}
else {
// there was an error verifying this
...
}
为nodejs回答这个问题,因为原始答案中存在细微的安全问题和一些缺失的逻辑(但非常有用(。这个答案解决了以下问题:
- 有人输入自己的URL,从而获得自己请求的身份验证
- CRC需要是一个无符号整数,而不是一个有符号整数
- NodeJs<17.0缺少一些内置的X509功能
- 理想情况下,应该使用内置的证书链验证签名证书但是NodeJS<17.0 AFAICT不能轻易做到这一点。信任模型依赖TLS和内置的nodejs信任链来获取证书URL,而不是从证书URL返回证书,这可能已经足够好了
const forge = require('node-forge');
const crypto = require('crypto')
const CRC32 = require('crc-32');
const axios = require('axios');
const transmissionId = paypalSubsEvent.headers['PAYPAL-TRANSMISSION-ID'];
const transmissionTime = paypalSubsEvent.headers['PAYPAL-TRANSMISSION-TIME'];
const signature = paypalSubsEvent.headers['PAYPAL-TRANSMISSION-SIG'];
const webhookId = '<your webhook ID from your paypal dev. account>';
const url = paypalSubsEvent.headers['PAYPAL-CERT-URL'];
const bodyCrc32 = CRC32.str(paypalSubsEvent.body);
const unsigned_crc = bodyCrc32 >>> 0; // found by trial and error
// verify domain is actually paypal.com, or else someone
// could spoof in their own cert
const urlObj = new URL(url);
if (!urlObj.hostname.endsWith('.paypal.com')) {
throw new Error(
`URL ${certUrl} is not in the domain paypal.com, refusing to fetch cert for security reasons`);
}
const validationString =
transmissionId + '|'
+ transmissionTime + '|'
+ webhookId + '|'
+ unsigned_crc;
const certResult = await axios.get(url); // Trust TLS to check the URL is really from *.paypal.com
const cert = forge.pki.certificateFromPem(certResult.data);
const publicKey = forge.pki.publicKeyToPem(cert.publicKey)
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(validationString);
verifier.end();
const result = verifier.verify(publicKey, signature, 'base64');
console.log(result);
您可以在Paypal API的中使用以下步骤
- 创建应用程序并从开发人员面板获取客户端ID和机密
- 在应用程序中创建Webhook并获取Webhook ID
实施PayPal APIhttps://www.postman.com/paypal/workspace/paypal-public-api-workspace/collection/19024122-92a85d0e-51e7-47da-9f83-c45dcb1cdf24?action=share&创建者=22959279
- 每次连接PayPal时,都可以借助客户端ID和密钥获得新的访问令牌
4.使用webhook Id、Access Token和request Headers来验证webhook
try{
$json = file_get_contents('php://input');
$data = json_decode($json);
$paypalmode = ($this->dev_mode == 0) ? '' : '.sandbox';
$API_Endpoint = 'https://api-m' . $paypalmode . '.paypal.com/v1/';
//step-01 get token
$res_token = getToken($API_Endpoint);//get Token mention in above postman link
//step-02 validate webhook
$webhook_id = 'XXXXXX';
$post_data = array(
"webhook_id" => $webhook_id ,
"transmission_id" => $_SERVER['HTTP_PAYPAL_TRANSMISSION_ID'],
"transmission_time" => $_SERVER['HTTP_PAYPAL_TRANSMISSION_TIME'],
"cert_url" => $_SERVER['HTTP_PAYPAL_CERT_URL'],
"auth_algo" => $_SERVER['HTTP_PAYPAL_AUTH_ALGO'],
"transmission_sig" => $_SERVER['HTTP_PAYPAL_TRANSMISSION_SIG'],
"webhook_event" => $data
);
$res = verifyWebhook($API_Endpoint . 'notifications/verify-webhook-signature',
$res_token['access_token'], $post_data);//use postman 'verify-webhook-signature' api mention in webhook section
if (isset($res->verification_status) && $res->verification_status == 'SUCCESS') {
//success
}else{
//failure
}
} catch (Exception $ex) {
//error
}
对此进行响应以避免潜在的麻烦,但上面的示例不起作用,因为需要将身份验证令牌与证书文件的get请求一起发送"CCD_ 2";不会单独工作。
只需在标头中包含您的身份验证令牌,它就可以工作了。