所有文章
深度解析閱讀時間 8 分鐘

在處理退款之前先驗證 App Store 伺服器通知,因為任何人都能向你的 webhook URL 發送請求

在檢查簽章之前,一則退款通知只不過是一個 HTTPS 請求。本文說明如何驗證 App Store 伺服器通知和 Google Play RTDN 訊息、各商店簽署的內容,以及偽造的退款或遭錯誤拒絕的退款會讓你付出什麼代價。

溫暖檯燈光線下,書桌上的筆電旁放著一枚黃銅火漆印章與一封已封緘的信封,象徵在信任 App Store 伺服器通知之前要先驗證它

重點摘要

  • 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 V2Google Play RTDN
訊息本身是否有簽署?是,signedPayload 是一個 JWS否,data 欄位是 base64 編碼的 JSON
如何證明傳送者鏈結至 Apple 根憑證的 x5c 憑證鏈推送時的 Pub/Sub OIDC 權杖,或你自己經過驗證的提取
巢狀的已簽署物件signedTransactionInfo 和 signedRenewalInfo,各自都是獨立的 JWS無
需與你的 App 相符的欄位bundleId、appAppleId、environmentpackageName
來源網路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 簽章。
  • 將解碼後的資料與你的 App 核對:bundleId、正式環境中的 appAppleId,以及 environment。即使 Apple 簽章有效,如果它屬於另一位開發人員的 App,或是一筆打到你正式環境端點的沙箱購買,那仍然不是你的退款。

接著對內部內容再做一次。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_REQUESTApple 在沒有你資料的情況下做出決定這筆銷售,以及為交付它已花費的一切
拒絕了真實的退單審查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 是否與你的 App 相符。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 以及你的 App 連結起來的檢查,是 JWS 簽章。

資料來源與延伸閱讀

RefundHalt

App Store 與 Google Play 的退款自動駕駛

繼續閱讀

下一筆退款申請已經在路上。

讀完另一封關於未能抗辯退款的客服郵件所需的時間,就足夠您設定好 RefundHalt。