在处理退款之前先验证 App Store 服务器通知,因为任何人都能向你的 webhook URL 发送请求
在检查签名之前,一条退款通知只不过是一个 HTTPS 请求。本文介绍如何验证 App Store 服务器通知和 Google Play RTDN 消息、每个商店签名的内容,以及伪造的退款或被错误拒绝的退款会让你付出什么代价。

要点
- Apple 以 signedPayload 的形式发送每一条第 2 版 App Store 服务器通知。它是一个 JSON Web Signature,由三个 Base64URL 部分组成:header、payload 和 signature。
- App Store 服务器通知的 JWS header 携带一条由三个证书组成的 x5c 证书链:签名用的叶证书、带有 Apple Worldwide Developer Relations 扩展 1.2.840.113635.100.6.2.1 的 Apple 中间证书,以及 Apple 根证书。
- Apple 的 App Store Server Library 使用 SignedDataVerifier 验证通知。它会检查证书链、bundle ID、environment,在生产环境中还会检查 appAppleId,并且可以在线检查证书吊销状态。
- Google Play 实时开发者通知并不由 Google Play 签名。在推送订阅中,Pub/Sub 可以在 Authorization header 中附带一个已签名的 OpenID Connect 令牌,由你的服务器检查其签名、audience 和 email 声明。
- Google 表示,RTDN 只告诉你某笔购买的状态发生了变化。因此,对任何退款通知的安全处理方式,是在撤销访问权限之前先通过 Google Play Developer API 确认。
- Apple 从 17.0.0.0/8 子网发送 App Store 服务器通知,但 IP 允许列表只能缩小可以访问你端点的范围。真正证明消息来自 Apple 的是签名。
- 错误拒绝真实消息的验证器,与接受伪造消息的验证器代价相当。因为你丢弃的 CONSUMPTION_REQUEST 只有 12 小时的响应期限,而 Google Play 拒付审核只有 24 小时。
任何知道你 webhook URL 的人都可以向它发送一条退款。这是每一条 App Store 服务器通知和每一条 Google Play 实时开发者通知背后令人不安的事实:在你验证之前,这条消息只是一个带有 JSON 的 HTTPS POST 请求。如果你的服务器仅仅因为某个请求写着 REFUND 就撤销订阅、收回额度或封禁账号,那么你就把这种权力交给了任何会写 curl 命令的人。
两个商店都提供了检查方法。Apple 会对通知本身签名,所以你只需要 Apple 的根证书就能验证 App Store 服务器通知。Google Play 走的是另一条路。消息本身没有签名,但 Pub/Sub 的投递可以签名,而且 Google 无论如何都要求你通过其 API 确认每一次变更。下面说明每种方式能证明什么,以及在任何一个方向上出错会付出什么代价。
每个商店实际签名的是什么
| 问题 | App Store Server Notifications V2 | Google Play RTDN |
|---|---|---|
| 消息本身是否签名? | 是,signedPayload 是一个 JWS | 否,data 字段是 base64 编码的 JSON |
| 如何证明发送方 | 指向 Apple 根证书的 x5c 证书链 | 推送时的 Pub/Sub OIDC 令牌,或你自己经过身份验证的拉取 |
| 嵌套的签名对象 | signedTransactionInfo 和 signedRenewalInfo,各自都是独立的 JWS | 无 |
| 需要与你的应用匹配的字段 | bundleId、appAppleId、environment | packageName |
| 来源网络 | 17.0.0.0/8,沙盒和生产环境 | Google Cloud Pub/Sub |
| 处理前是否需要确认 | 可选,签名数据本身就是记录 | 必需,Google 要求调用 Developer API |
| 传输方式 | HTTPS,TLS 1.2 或更高版本 | HTTPS 推送,或客户端库拉取 |
如何验证 App Store 服务器通知
Apple 的文档将 signedPayload 描述为由三个以句点分隔的 Base64URL 编码部分组成的字符串:header、payload 和 signature,并按照 JSON Web Signature 规范签名。解码 payload 很容易。要信任它,则需要四项检查。
- 从解码后的 header 中读取 x5c 数组。Apple 记录了其顺序:首先是包含签名密钥的证书,然后是 Apple 中间证书,最后是 Apple 根证书。
- 使用你自己从 Apple PKI 网站下载的 Apple 根证书(实际上就是 Apple Root CA - G3)验证这条证书链。绝不要信任随消息一起到达的根证书,因为伪造者可以在其中放入任何证书。
- 确认中间证书带有 Apple Worldwide Developer Relations 扩展 1.2.840.113635.100.6.2.1,然后用叶证书的公钥验证 JWS 签名。
- 将解码后的数据与你的应用进行核对:bundleId、生产环境中的 appAppleId,以及 environment。即使是有效的 Apple 签名,如果属于另一位开发者的应用,或者是一笔打到你生产端点的沙盒购买,那也仍然不是你的退款。
然后对内部内容再做一遍。data 对象包含 signedTransactionInfo 和 signedRenewalInfo,它们各自都是独立的 JWS,带有自己的 x5c 证书链。如果 REFUND 处理程序验证了外层,却在未验证的情况下读取交易信息,就在中间留下了漏洞。
使用 Apple 的库,而不是自己编写
Apple 为 Swift、Java、Python 和 Node 发布了 App Store Server Library。它的 SignedDataVerifier 接收你的根证书、在线检查开关、environment、你的 bundle ID,以及生产环境下的 appAppleId。Node 源码展示了它强制执行的内容:证书链中恰好三个证书、Apple 叶证书扩展 1.2.840.113635.100.6.11.1 和中间证书扩展、证书有效期,以及匹配的 bundle ID、app ID 和 environment。开启在线检查后,它还会向 OCSP 响应方查询叶证书和中间证书是否已被吊销。

IP 允许列表够用吗?
不够。Apple 表示,如果你的服务器需要允许列表,应添加 17.0.0.0/8 子网,并且它同时适用于沙盒和生产环境。这能减少噪音,但无法证明消息是真实的,而且一旦 Apple 的网络以你未获知的方式发生变化,它就会失效。请把签名检查作为真正的关卡。
如何验证 Google Play 退款通知
Google Play 将 RTDN 消息发布到你拥有的 Pub/Sub 主题,并且只有 google-play-developer-notifications@system.gserviceaccount.com 这个服务账号应在该主题上拥有 Pub/Sub Publisher 角色。随后,你的订阅会以两种方式之一投递消息,每种方式都有各自的证明手段。
- 拉取订阅:你的服务器与 Pub/Sub 建立经过身份验证的连接并获取消息。这里没有公开端点,所以陌生人无处可发。
- 开启身份验证的推送订阅:Pub/Sub 会签发一个 JSON Web Token,并以 Bearer 令牌的形式放在 Authorization header 中发送。Google 的指南要求验证令牌的签名,并检查其 email 和 audience 声明是否与你配置的值一致,其示例代码还要求 email_verified 为 true。该令牌最长可能已签发一小时。
- 未开启身份验证的推送订阅:任何能够访问该 URL 的人都可以发送一条看起来像 RTDN 的消息。
为什么 Google 要求你通过 API 确认
即使是身份验证完全正确的 RTDN,也不包含完整的信息。Google 的设置指南明确指出,这些通知只告诉你某笔购买的状态发生了变化,你必须调用 Google Play Developer API 来获取完整状态。对于 voidedPurchaseNotification,这意味着在撤销任何内容之前,先在 Voided Purchases API 中找到这笔购买,或获取其当前状态。API 调用使用你自己的服务账号进行身份验证,所以它的结果是伪造者无法伪造的。
伪造的退款或被拒绝的退款在金钱上的代价
一条你信以为真的伪造退款通知,会让你失去一位付费客户。你锁定了一个你仍然收着其钱款的用户,对方会提交支持工单,其中一些人会去商店申请你的系统早已假装批准过的退款。伪造的购买或续订则造成相反方向的损失:你在根本不存在的收入基础上交付 GPU 算力、付费第三方 API 调用、存储空间和创作者分成。
相反的失败更隐蔽,往往代价也更大。一个因为 appAppleId 错误、在生产环境中使用沙盒设置或根证书过时而拒绝真实消息的验证器,会丢掉两个最需要你提供证据的退款流程。Apple 要求在收到 CONSUMPTION_REQUEST 后 12 小时内提供消费数据。Google 给你 24 小时通过 orders.reviewrefund 回应拒付审核,而对于 2026 年 8 月 3 日之后下的订单,输掉一次 Google Play 拒付将让你损失购买价格减去 Play 服务费后的金额,外加买家银行收取的费用。
| 失败情形 | 会发生什么 | 代价是什么 |
|---|---|---|
| 接受了伪造的 REFUND | 付费客户失去访问权限 | 支持工作时间,以及一个由你招来的真实退款申请 |
| 接受了伪造的购买 | 免费获得付费功能 | 没有收入支撑的算力、API 调用、存储和分成 |
| 拒绝了真实的 CONSUMPTION_REQUEST | Apple 在没有你的数据的情况下做出决定 | 这笔销售,以及为交付它已经花掉的一切 |
| 拒绝了真实的拒付审核 | Google 在没有你的证据的情况下做出决定 | 2026 年 8 月 3 日之后订单的购买价格和银行费用 |
不要让验证错误错过截止期限
- 在验证之前先保存原始请求,这样被拒绝的消息就永远不会真正丢失。
- 对每一次验证失败发出警报。一条伪造消息只是噪音。所有消息同时失败,则是你自己的错误。
- 在 Apple 一侧,你可以重放被拒绝的消息。Get Notification History 会返回过去 180 天内的原始 signedPayload,因此修复验证器后,可以把已保存或恢复的 payload 再次送入验证。
- 在 Google 一侧,待处理的拒付审核只存在于其通知中,所以推送 audience 配置错误就意味着错过一次审核。每次更改后,都请在 Play Console 中使用 Send Test Message 进行测试。
RefundHalt 会验证两个商店的通知,在任何更改发生之前通过商店 API 确认退款,并在期限内回应 CONSUMPTION_REQUEST 和拒付审核。这样,伪造的消息不会让你失去客户,真实的消息也不会被漏掉。
常见问题解答
- 如何验证 App Store 服务器通知?
- 将 signedPayload 作为 JSON Web Signature 进行验证。从其 header 中读取 x5c 证书链,使用你从 Apple PKI 网站下载的 Apple 根证书验证该证书链,用叶证书的密钥验证签名,然后检查 bundleId、appAppleId 和 environment 是否与你的应用一致。Apple 的 App Store Server Library 在其 SignedDataVerifier 中完成了所有这些步骤。
- 验证 App Store 服务器通知需要使用哪个密钥?
- 你不需要来自 App Store Connect 的密钥。公钥位于 JWS header 内 x5c 证书链的第一个证书中,只有在你将该证书链一直验证到你自己获取的 Apple 根证书之后,才能信任它。你的 App 内购买密钥用于调用 Apple 的 API,而不是用于检查通知。
- Google Play 实时开发者通知有签名吗?
- 没有,Google Play 不会对 RTDN 消息本身签名。在开启身份验证的推送订阅中,Pub/Sub 会在 Authorization header 中发送一个已签名的 OpenID Connect 令牌供你验证,并且 Google 要求你在每次收到通知后调用 Google Play Developer API,以获取该购买的完整状态。
- 当通知验证失败时,我的服务器应该返回错误吗?
- 先保存原始请求,然后再决定。在收到 40x 或 50x 响应后,Apple 会重试第 2 版通知,分别在上一次尝试之后的 1、12、24、48 和 72 小时进行。如果失败原因是你自己的配置,Get Notification History 可以让你在 180 天内恢复原始 signedPayload,并在修复后重新验证。
- 如果我只允许 Apple 的 IP 范围,还需要验证通知吗?
- 需要。Apple 要求使用允许列表的服务器接受 17.0.0.0/8 子网,这可以限制谁能连接,但无法证明消息是真实的。将一条通知与 Apple 以及你的应用关联起来的检查,是 JWS 签名。
来源和延伸阅读
- Apple Developer: signedPayload
- Apple Developer: JWSDecodedHeader
- Apple Developer: Enabling App Store Server Notifications
- Apple Developer: Responding to App Store Server Notifications
- Apple on GitHub: App Store Server Library (Node)
- Android Developers: Get ready (Real-time developer notifications setup)
- Google Cloud: Authentication for push subscriptions
- Play Console Help: Updates to refund protection and chargeback cost responsibility
RefundHalt
App Store 和 Google Play 退款自动驾驶