跳到内容

如何在 PHP 中验证 Payora 签名 Webhook

了解如何在 PHP 中读取原始载荷、计算并比较 HMAC 签名,并处理时间戳、重放保护、HTTP 响应与常见验证错误。

Payora11 分钟阅读EN · RU · UK · ES · DE · FR · IT · PL · PT · HI · ZH
如何在 PHP 中验证 Payora 签名 Webhook

什么是 Payora 签名 Webhook

Payora 签名 Webhook 是一种携带数字签名的 HTTP 请求,因此你的 PHP 端点可以检查事件是否确实来自 Payora,以及传输过程中是否遭到篡改。如果你正在了解如何用 PHP 验证 Payora Webhook 签名,基本流程很简单:Payora 发送事件,服务器读取原始载荷,然后代码验证签名,确认无误后才信任请求正文中的内容。

这项检查很重要,因为任何知道你 URL 的客户端都可能向你的 Webhook 发送请求。恶意用户可以从日志中抄下端点地址,再 POST 一条 JSON 格式看似正常的虚假支付事件。签名可以阻止这种伎俩。

只有验证通过后,事件本身才有用。在此之前,它只是一段文本。

可以把它想象成门口的一张编号票据:正文是消息,签名是印章,而你的 PHP 代码负责判断两者是否匹配。匹配就处理事件,不匹配就立即拒绝。

如果你的网站已经有支付流程,这种做法也适用于技术栈的其他环节。阅读加密货币电商支付网关指南的商家也会看到同样的原则:少接收,多验证,确认无误后再更新订单。

PHP Webhook 端点的前置要求

你不需要庞大的框架。PHP 8.x 就足以搭建端点,有些项目也可以使用 PHP 7.4;但代码必须能够访问原始请求正文以及 Payora 发送的请求头。这意味着服务器应支持常规 HTTP 请求访问,而不能使用会改写或丢弃请求头的自定义层。

请使用 HTTPS。没有 TLS,Webhook 数据就会以明文传输,而仅靠签名并不能弥补传输安全方面的不足。配置好证书和虚拟主机,并确保公开 URL 可以正常访问,这是最基本的要求。

实际使用中有两个 PHP 扩展值得关注:用于 HMAC 运算的 hash 扩展,以及在处理 JSON 时用到的 PHP 内置 JSON 函数。你也可以使用 cURL 从自己的环境进行测试,但 Webhook 端点本身并不需要它。

你还需要与 Payora 账户中 Webhook 配置相匹配的 Payora 密钥或签名密钥。请将它放在网站根目录之外,也不要提交到 Git。把密钥放进公开仓库,会让签名验证形同虚设。

还有一点:端点应只接受 POST 请求。GET 请求可以返回 405 状态码。这项简单限制能缩小排查范围,让你只需关注真正需要检查的流量。

获取原始 Webhook 载荷和请求头

签名检查依赖 Payora 实际发送的字节内容。在 PHP 中,使用 file_get_contents('php://input') 读取原始请求正文。不要通过 $_POST 或解码后的 JSON 重新构造载荷,因为这两种操作都可能改变空格、键的顺序或编码。

请求头也要遵循同样的原则。你需要读取签名请求头。环境支持时可使用 getallheaders();如果环境会过滤这个辅助函数返回的请求头,则可以读取 $_SERVER['HTTP_X_...'] 之类的服务器变量。不同主机环境之间的小差异也可能影响结果。

假设 Webhook 正文有 412 个字节。如果代码先解码再重新编码,看到的字段可能完全一样,但字节内容已经不再相同。仅这一处差异就足以导致验证失败。

请牢记这个顺序:先获取,再解析。首先保存原始字符串,然后读取请求头,之后再处理载荷结构。

上线前测试支付流程时,这个习惯也能帮你节省时间。关于如何测试加密货币支付的文章也将这一原则应用于沙盒事件:保持载荷原样,测试才有意义。

在 PHP 中重新计算预期签名

常见做法是使用原始载荷和共享密钥计算 HMAC。首先,按照 Payora Webhook 文档指定的算法进行计算。文档要求使用 SHA-256,就用 SHA-256;如果文档规定了前缀、版本标记或时间戳格式,也要严格按规定添加。这就是Payora Webhook HMAC 签名验证方法的核心:使用正确的原始数据、密钥与算法,按规范计算并验证结果。

常见的格式是:先用时间戳和原始正文构造待签名消息,再使用共享密钥和指定的哈希算法调用 hash_hmac()。根据规范,结果可能还需要进行十六进制或 Base64 编码。不要猜测格式,应以 Webhook 设置中的说明为准。

下面是一个简化的 PHP 示例,其中预留了可能因 Payora 格式而异的部分:

$rawBody = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_PAYORA_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_PAYORA_SIGNATURE'] ?? '';
$secret = getenv('PAYORA_WEBHOOK_SECRET');

$signedPayload = $timestamp . '.' . $rawBody;
$expected = hash_hmac('sha256', $signedPayload, $secret);

这个示例只展示了大致形式,并不保证与 Payora 的实现完全一致。如果 Payora 只对载荷签名,就从字符串中移除时间戳;如果 Payora 对规范化后的 JSON 签名,就必须严格遵循其方法,而不是使用你偏好的方式。分隔符哪怕只错一个,所有请求都会验证失败。

对于也关注账单规则和边界情况的商家,关于加密货币支付账单限额的文章也很有参考价值,因为 Webhook 处理和账单策略往往会在订单生命周期的同一环节交汇。

将计算出的签名与 Payora 的签名进行比较

得到预期签名后,应使用恒定时间比较方法,将它与收到的签名进行比较。在 PHP 中,大多数情况下应使用 hash_equals()。这里不适合使用简单的 == 或 ===:这类比较可能泄露时间信息,也无法同样稳妥地处理细微的差异情况。

比较之前,必须严格按照 Payora 的要求规范签名格式。如果请求头中的签名是十六进制格式,就比较十六进制字符串;如果是 Base64 格式,就比较 Base64 字符串。如果收到的签名带有 sha256= 这样的前缀,就按文档要求决定是否保留。

处理流程示例:

  1. 读取原始正文。
  2. 读取签名请求头。
  3. 根据文档规定的公式计算预期签名。
  4. 使用 hash_equals() 比较两个字符串。
  5. 如果两者不匹配,则返回 401 或 400,拒绝请求。

签名错误时,返回 401 很合适;缺少必需请求头时,返回 400 更合理。无论哪种情况,响应内容都应简短,不要泄露额外线索。

这里记住一句话就够了:千万不要打印密钥。这种错误虽然少见,但后果极其严重。

处理时间戳、重放保护和容差规则

如果 Payora 包含时间戳或随机数(nonce),应在接受事件前进行检查。要了解PHP Webhook 如何防止重放攻击,关键是验证时间戳是否在允许的时间范围内,并在使用 nonce 时记录并拒绝重复值。这样可以阻止攻击者稍后重新发送一条有效 Webhook,试图诱使应用重复付款或重复发货。时间戳的有效范围应遵循 Payora 的规定,而不是凭猜测设置。

常见做法是将 Webhook 时间戳与服务器时间进行比较,拒绝超出容差范围的过期请求。如果文档规定 5 分钟,就用 5 分钟;如果规定 300 秒,就把 300 秒设为一个有明确名称的配置项,避免在三个文件里硬编码一个来历不明的数字。

时钟偏差会导致正常请求被拒绝。服务器时间如果慢了 7 分钟,就可能拒绝有效请求。请使用 NTP 同步服务器,并确保时区设置一致。时区不会改变 Unix 时间戳,但本地时区配置错误仍会让日志更难阅读。

如果使用 nonce,处理起来就更简单。保存每个 nonce 并标记为已使用,遇到重复值就拒绝。简单的缓存或数据库表即可胜任。一个 nonce,只接受一次。

如果你也在支付页面上采用固定价格,这时就要考虑相关策略。关于是否应该采用固定价格的文章,可以帮助你协调汇率规则与 Webhook 的时间处理,避免延迟事件导致确认了错误金额。

处理有效事件并返回正确的 HTTP 响应

验证通过后,再解析载荷,并根据事件类型分流处理。payment.succeeded 事件不应与 refund.updated 事件走同一套代码路径。请清晰地拆分处理逻辑,让每个处理器各司其职,日志也更易于阅读。

发送 2xx 响应前,应先完成业务逻辑。如果要将订单标记为已付款,先写入数据库记录;如果要发送邮件,先将任务加入队列。任何一步失败,都应返回 500,让 Payora 稍后重试。

这个顺序很重要,因为 2xx 响应通常表示 Payora 已收到并接受该事件。如果过早返回 200,随后数据库写入却失败,就可能导致 Payora 与应用中的记录不一致。这种问题凌晨两点发生时,代价可不小。

响应正文保持简短即可,返回“OK”就行。更好的做法是不返回正文,只用清晰的状态码表示结果。不要把载荷原样回显。

如果团队还会根据支付事件向客户开具账单,自由职业者加密货币支付网关这篇文章展示了如何让 Webhook 从技术事件触发实际账单操作,同时避免增加不必要的步骤。

排查签名验证失败

大多数失败都源于五种情况:原始正文被更改、请求头名称错误、密钥错误、编码不一致,或时间戳超出容差范围。先检查正文。如果记录了原始载荷和计算出的签名,应先比较它们,再去检查数据库。

正文被修改的情况很常见。框架中间件可能会删除空白、解码 JSON 或规范化换行符。多一个换行符就足以导致失败。如果 Payora 对 {"a":1} 签名,而应用读取到的是 {"a":1}\n,HMAC 就无法匹配。

接下来检查请求头是否匹配。有些服务器会将连字符转换成下划线;有些代理层则会删除未明确允许的自定义请求头。如果 PHP 中读不到签名请求头,请检查 Web 服务器配置和代理设置,而不只是代码。

编码问题不易察觉。UTF-8 字符和转义后的 Unicode 在日志中看起来可能完全正常,但实际字节内容仍可能不同。如果应用重新处理了 JSON,即使载荷在浏览器中看起来相同,签名也可能失效。

密钥错误是最简单的原因,却常常最后才被检查。请分别核对环境变量、测试环境密钥和生产环境密钥。从错误账户复制密钥,足以让你白白排查一下午。

时钟偏差会表现出另一种症状:签名匹配,但请求仍因时间戳过期而被拒绝。遇到这种情况,请检查 NTP、容器时钟,以及任何会添加自身时间戳的反向代理。基础设施的细微时钟偏差也可能看起来像签名错误。

如果你的安装还集成了 WordPress 或会员账单功能,关于如何接受 USDT 付款的文章也很有参考价值,因为它展示了如何在一个完整流程中衔接 Webhook 处理、访问控制和支付确认。

最后还有一项很有帮助的测试:将同一载荷发送两次。第一次请求应通过。如果你保存并拒绝重复使用的 nonce,第二次就应失败;如果你的 Webhook 设计不使用基于 nonce 的重放防护,则第二次请求可能仍会通过。这一次重复请求就能帮你确认防护措施是否真正有效。

Comments

准备开始了吗?

创建账户,并在一小时内开出第一张账单。

此页面回答的问题