集成 Paypal 的智能按钮后,我无法验证 Paypal 发送的 webhook 通知。我发现的示例要么已过时,要么不起作用。
有没有办法验证 webhook 通知,最好是 DIY 方式(即无需使用庞大而复杂的 Paypal API)?
集成 Paypal 的智能按钮后,我无法验证 Paypal 发送的 webhook 通知。我发现的示例要么已过时,要么不起作用。
有没有办法验证 webhook 通知,最好是 DIY 方式(即无需使用庞大而复杂的 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 所理解的不完全相同。Paypal 将其称为“sha256WithRSA”,但openssl_verify
预计会出现“sha256WithRSAEncryption”。
// 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回答这个问题,因为原始(但非常有用)的答案存在微妙的安全问题和一些缺失的逻辑。这个答案解决了以下问题:
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);
对此做出响应以节省潜在的麻烦,但上面的示例不起作用,因为需要将身份验证令牌与您对证书文件“ file_get_contents($header['Paypal-Cert-Url'])
”的获取请求一起发送将无法单独工作。
只需在标头中包含您的身份验证令牌即可。