蘋果內購 V1 與 V2 支付流程對比(附示例java 8+代碼)
官方文檔:https://developer.apple.com/documentation/appstoreserverapi
國內大部分開發者對 微信支付、支付寶支付 的流程都比較熟悉,其典型的三步為:
- 客户端調用服務端預下單接口
服務端會創建訂單並調用微信/支付寶的下單 API,獲取支付數據。 - 客户端拉起支付
客户端使用第一步返回的支付數據,調起微信/支付寶進行支付。 - 異步通知 + 業務處理
用户支付完成後,微信/支付寶的服務器會向我們的服務端發起異步回調,通知支付結果,服務端確認後完成發貨。
蘋果內購第一版本(V1)
在 V1 中,蘋果內購的支付邏輯與微信、支付寶有明顯不同:
- 添加內購商品
在蘋果開發者後台(App Store Connect)配置商品 ID、價格等。 - 客户端直接發起支付
客户端展示商品列表,用户選擇商品後,直接調用蘋果內購 API 發起支付(不需要預下單接口)。 -
收據驗證
- 支付成功後,蘋果返回一個 支付憑證(receipt-data) 給客户端。
- 客户端需要將憑證上傳到服務端。
- 服務端再調用蘋果的
verifyReceipt接口驗證訂單是否真實有效,完成支付流程。
⚠️ 問題:沒有標準化異步回調,容易掉單
- 蘋果在 V1 時代幾乎沒有完善的異步通知機制(只有訂閲類有 Server Notification V1,且字段混亂不標準)。
-
這意味着:
- 如果客户端因網絡問題未上傳收據,服務端就無法感知訂單 → 容易掉單。
- 如果客户端惡意攔截上傳,服務端也會遺漏訂單。
- 因此,V1 的支付鏈路依賴客户端可靠上傳憑證,存在較大業務風險。
蘋果內購第二版本(V2)
在 V2(App Store Server API + Server Notification V2)中,蘋果對支付流程做了升級,更加接近微信/支付寶的邏輯:
- 添加內購商品
在蘋果後台配置商品 ID、價格等。 -
客户端調用服務端預下單接口
- 客户端下單時,先請求我們的服務端。
- 服務端生成一條訂單記錄,並生成一個 UUID 格式的唯一標識(例如
123e4567-e89b-12d3-a456-426614174000)。 -
注意區別:
- 微信/支付寶的預下單是調用官方 API 獲取支付參數;
- 蘋果內購的預下單不需要和蘋果交互,僅返回一個 UUID 給客户端 (蘋果官方強制要求為UUID格式),iOS客户端用此UUID和商品 ID 一起發起支付。
- 如果系統的訂單號不是 UUID,需要額外生成一個 UUID 與訂單號關聯。
-
支付驗證 + 異步回調
- 客户端完成支付後,蘋果返回一個 支付憑證,客户端將其傳給服務端進行驗證;
- 同時,如果在蘋果後台配置了 Server Notification V2 回調地址,蘋果也會主動將訂單憑證推送給服務端;
- 服務端要有分佈式鎖或冪等控制,客户端上傳憑證或蘋果回調任意一方先到,均可完成發貨,避免掉單。
⚠️ 蘋果 V2 異步回調的特殊點
- 回調地址 只能在蘋果後台配置,不能像微信/支付寶那樣在每次下單時自由指定。
-
只能配置一個回調地址:
- 正式環境下通常配置生產環境域名;
- 如果已經上線,回調域名配的是正式服的,在測試環境進行支付,回調依然會打到生產環境 → 生產環境無法匹配到訂單,需要額外邏輯來區分(這一點需要自行編碼驗證後才能更深入瞭解)。
V1 與 V2 的流程對比
| 特性 | V1(舊版) | V2(新版) |
|---|---|---|
| 憑證獲取 | 客户端支付後獲取 receipt(Base64) |
客户端支付後獲取 transactionId |
| 驗證方式 | 客户端上傳 receipt 到服務端 → 服務端調用 Apple 接口驗證 |
服務端通過 transactionId 調用 Apple Server API 查詢訂單 |
| 異步通知 | ❌ 不支持 | ✅ 支持,Apple 會推送 signedPayload |
| 環境區分 | production / sandbox | production / sandbox + Apple TestFlight |
| 冪等機制 | 需自行實現,難度大 | transactionId 唯一,全局可冪等 |
| 風險點 | 客户端可偽造請求 → 容易被破解 | 服務端直連 Apple 驗證 + JWT 簽名,安全性更高 |
👉 可以看出,V2 的設計更標準化,和微信/支付寶非常接近。
V2 訂單驗證流程
整體步驟如下:
- 客户端調用服務端預下單接口,生成訂單信息並返回UUID給客户端
- 客户端支付完成 → 獲取 transactionId
- 客户端上傳 transactionId 給服務端
- 服務端調用 Apple Server API 驗證
- 服務端處理支付邏輯(冪等控制)
- Apple 異步通知(signedPayload),再次確認交易狀態(異步通知與客户端上傳
transactionId可能同時進行,需分佈式鎖做好冪等性)
接下來將用代碼來演示蘋果內購V2版本如何驗證客户端上傳的支付憑據和異步回調的支付結果
準備工作(除了rootCAG2和rootCAG3可自己下載,其餘參數登錄蘋果控制枱獲取即可)
keyId: 在蘋果控制枱獲取的keyId,格式如6G8VD0TVY
issuerId: 在蘋果控制枱獲取的issuerId,格式為UUID,123e4567-e89b-12d3-a456-426614174000
bundleId: 自己蘋果app的包名,如com.xxx.xxx
signingKey: 在蘋果控制枱下載的密鑰,如SubscriptionKey_6G8VD0TVY.p8
appAppleId: 在蘋果控制枱獲取的appAppleId
#以下兩個文件直接下載,公共文件,不是每個蘋果賬户獨有的,訪問地址:https://www.apple.com/certificateauthority/
rootCAG2: 下載地址:https://www.apple.com/certificateauthority/AppleRootCA-G2.cer
rootCAG3: 下載地址:https://www.apple.com/certificateauthority/AppleRootCA-G3.cer
java 11+ 引入以下依賴(查看git地址,獲取最新版本)
<dependency>
<groupId>com.apple.itunes.storekit</groupId>
<artifactId>app-store-server-library</artifactId>
<version>3.5.0</version>
</dependency>
git地址:https://github.com/apple/app-store-server-library-java
java 8+ 引入以下依賴(查看git地址,獲取最新版本)
<dependency>
<groupId>io.github.andyoconnor</groupId>
<artifactId>app-store-server-library-java8</artifactId>
<version>3.5.2-RELEASE</version>
</dependency>
git地址:https://github.com/AndyOConnor/app-store-server-library-java8
本次演示環境:jdk 17,springboot 3.2.0,maven 3.9
1、ios客户端調用預下單接口獲取UUID後,會調用蘋果內購的sdk進行支付,用户支付完成時,蘋果會回調給客户端支付憑證,其中有個transactionId字段,是蘋果的內購單號,需要傳給服務端
2、服務端拿到transactionId後,開始校驗
import com.apple.itunes.storekit.client.AppStoreServerAPIClient;
import com.apple.itunes.storekit.client.APIException;
import com.apple.itunes.storekit.client.AppStoreServerAPIClient;
import com.apple.itunes.storekit.model.Environment;
import com.apple.itunes.storekit.model.TransactionInfoResponse;
import com.apple.itunes.storekit.model.*;
import com.apple.itunes.storekit.verification.SignedDataVerifier;
// 方法示例
public String getAppAccountToken(String transactionId, Environment environment) {
// 上述配置的值
String issuerId = "123e4567-e89b-12d3-a456-426614174000";
String keyId = "6G8VD0TVY";
String bundleId = "com.xxx.xxx";
Path filePath = Path.of("/path/to/key/SubscriptionKey_6G8VD0TVY.p8");
String encodedKey = Files.readString(filePath);
Set<InputStream> rootCAs = Set.of(
new FileInputStream("/path/to/rootCAG2"),
new FileInputStream("/path/to/rootCAG3")
);
String appAppleId = "";
try {
AppStoreServerAPIClient client = new AppStoreServerAPIClient(encodedKey, keyId, issuerId, bundleId, environment);
TransactionInfoResponse transactionInfo = client.getTransactionInfo(transactionId);
SignedDataVerifier signedPayloadVerifier = new SignedDataVerifier(
rootCAs, bundleId, appAppleId, environment, true
);
JWSTransactionDecodedPayload decodedPayload = signedPayloadVerifier.verifyAndDecodeTransaction(transactionInfo.getSignedTransactionInfo());
// 這個是預下單時服務端傳給ios客户端的UUID
return decodedPayload.getAppAccountToken().toString();
} catch (APIException e) {
long errorCode = e.getApiError().errorCode();
// 錯誤碼'4040010'表示訂單不存在。
// 由於客户端傳過來的transactionId,服務端並不知道是沙箱環境還是正式環境,這裏返回空字符串。
// 外部第一次調用可傳入Environment.PRODUCTION正式環境,返回空字符串時,可繼續傳入Environment.SANDBOX,
// 如果還是空字符串,就説明訂單號不是蘋果訂單號
if (errorCode == 4040010L) {
return "";
}
// 打印日誌方便排查問題
log.error("Apple getAppAccountToken error, errorCode: {}, transactionId: {}", errorCode, transactionId, e);
throw new RuntimeException(e);
} catch (Exception e) {
log.error("Apple getAppAccountToken error, transactionId: {}", transactionId, e);
throw new RuntimeException("Apple verify order fail", e);
}
}
// 蘋果的沙箱環境跟我們的測試環境不一樣,ios客户端分為測試包與正式包,測試包只能用沙箱環境支付,
// 正式包可以用正式環境支付,如果在蘋果控制枱配置了測試號,用該測試號登錄蘋果手機,在正式包環境支付時,返回的憑證中,環境是沙箱環境,
// 所以一個訂單號有可能是正式環境的,也可能是沙箱環境的,需要校驗兩次
public void verifyOrder(String transactionId) {
// 一般情況下,當app上架運營時,正式環境支付的比例要大於沙箱環境。
// 優先用正式環境校驗,基本上都會有值,不會再一次調用沙箱環境,除非是我們自己測試的訂單,佔比較少
String orderUUID = getAppAccountToken(transactionId, Environment.PRODUCTION);
// 如果正式環境沒拿到數據,繼續傳入沙箱環境
if (orderUUID == null || "".equals(orderUUID)) {
orderUUID = getAppAccountToken(transactionId, Environment.SANDBOX);
}
if (orderUUID == null || "".equals()) {
log.error("Apple verifyOrder error, transactionId: {}", transactionId);
throw new RuntimeException("Apple verify order fail");
}
// 拿到UUID後,就可以像微信/支付寶的異步回調一樣,查詢我們的訂單,執行發貨發幣等業務邏輯
}
異步回調
蘋果的異步回調中,會以post的方式,帶上signedPayload參數,該參數的值是一長串加密後的支付憑證
{
"signedPayload": ""
}
獲取到參數後,直接解密
import com.apple.itunes.storekit.model.NotificationTypeV2;
import com.apple.itunes.storekit.model.*;
import com.apple.itunes.storekit.verification.SignedDataVerifier;
// 方法示例
public void getDecodedNotificationPayload(String signedTransactionInfo) {
// 上述配置的值
String issuerId = "123e4567-e89b-12d3-a456-426614174000";
String keyId = "6G8VD0TVY";
String bundleId = "com.xxx.xxx";
Path filePath = Path.of("/path/to/key/SubscriptionKey_6G8VD0TVY.p8");
String encodedKey = Files.readString(filePath);
Set<InputStream> rootCAs = Set.of(
new FileInputStream("/path/to/rootCAG2"),
new FileInputStream("/path/to/rootCAG3")
);
String appAppleId = "";
// 在開發調試期間,如果在蘋果控制枱配置的是測試服的鏈接,則使用沙箱環境,上線後要使用正式環境
Environment environment = Environment.PRODUCTION
try {
SignedDataVerifier signedPayloadVerifier = new SignedDataVerifier(
rootCAs, bundleId, appAppleId, environment, true
);
ResponseBodyV2DecodedPayload decodedPayload = signedPayloadVerifier.verifyAndDecodeNotification(signedTransactionInfo);
// 這裏獲取到的交易信息與上面的的訂單校驗數據一致,調用上面訂單驗證的方法
String transactionInfo = decodedPayload.getData().getSignedTransactionInfo();
// 這是預下單時傳給蘋果的UUID,拿到後就可以查詢出訂單進行發貨發幣等業務了
String appAccountToken = getAppAccountToken(transactionInfo, environment);
// 蘋果的異步回調中,type有很多種類型,可以查看文章開頭的官方文檔瞭解,
// 或者查看NotificationTypeV2源碼
NotificationTypeV2 notificationType = decodedPayload.getNotificationType();
// 不同的回調類型處理不同的業務邏輯,這裏強烈建議使用策略模式,在我的設計模式專欄中最後一篇文章有介紹
// 支付下單和異步回調的策略模式用法,可參閲
if (notificationType == NotificationTypeV2.ONE_TIME_CHARGE) {
// 這裏是一次性內購的回調
} elseif(notificationType == NotificationTypeV2.REFUND) {
// 這裏是退款的回調
}
} catch (Exception e) {
log.error(
"{}->getDecodedNotificationPayload error, environment: {}, signedTransactionInfo: {}",
getClass().getSimpleName(), environment.name(), signedTransactionInfo, e);
throw new RuntimeException(e);
}
}
附:設計模式專欄(五):設計模式在實際項目中的應用 —— 支付系統擴展與回調處理案例
總結:
藉助蘋果的app-store-server-library java sdk,實際上用幾行代碼就能解密和驗證蘋果的支付憑證,難點在於瞭解蘋果內購V2的支付流程,以及事先需要準備的參數,以下是本次流程和代碼的總結:
1、在蘋果控制枱配置產品id、價格等商品信息
2、在蘋果控制枱以及相應網站複製對應參數和下載對應文件
keyId: 在蘋果控制枱獲取的keyId,格式如6G8VD0TVY
issuerId: 在蘋果控制枱獲取的issuerId,格式為UUID,123e4567-e89b-12d3-a456-426614174000
bundleId: 自己蘋果app的包名,如com.xxx.xxx
signingKey: 在蘋果控制枱下載的密鑰,如SubscriptionKey_6G8VD0TVY.p8
appAppleId: 在蘋果控制枱獲取的appAppleId
#以下兩個文件直接下載,公共文件,不是每個蘋果賬户獨有的,訪問地址:https://www.apple.com/certificateauthority/
rootCAG2: 下載地址:https://www.apple.com/certificateauthority/AppleRootCA-G2.cer
rootCAG3: 下載地址:https://www.apple.com/certificateauthority/AppleRootCA-G3.cer
3、iOS客户端支付完成會校驗一次支付憑證,蘋果官方也會異步回調,我們需要做好分佈式鎖,客户端主動校驗或蘋果官方異步回調有一個成功即可
4、蘋果的異步回調會有很多類型,如ONE_TIME_CHARGE一次性購買、REFUND退款、SUBSCRIBED訂閲等,其中訂閲也分一次性訂閲,自動續訂等,需要根據自己的實際業務,再結合官方文檔,摸索清楚