altosadventure免费中文版
84.52M · 2025-09-11
本教程的代码示例依赖苹果java sdk,并要求java 11+,java 8教程整理中
官方文档:developer.apple.com/documentati…
国内大部分开发者对 微信支付、支付宝支付 的流程都比较熟悉,其典型的三步为:
在 V1 中,苹果内购的支付逻辑与微信、支付宝有明显不同:
添加内购商品 在苹果开发者后台(App Store Connect)配置商品 ID、价格等。
客户端直接发起支付 客户端展示商品列表,用户选择商品后,直接调用苹果内购 API 发起支付(不需要预下单接口)。
收据验证
verifyReceipt
接口验证订单是否真实有效,完成支付流程。苹果在 V1 时代几乎没有完善的异步通知机制(只有订阅类有 Server Notification V1,且字段混乱不标准)。
这意味着:
因此,V1 的支付链路依赖客户端可靠上传凭证,存在较大业务风险。
在 V2(App Store Server API + Server Notification V2)中,苹果对支付流程做了升级,更加接近微信/支付宝的逻辑:
添加内购商品 在苹果后台配置商品 ID、价格等。
客户端调用服务端预下单接口
客户端下单时,先请求我们的服务端。
服务端生成一条订单记录,并生成一个 UUID 格式的唯一标识(例如 123e4567-e89b-12d3-a456-426614174000
)。
注意区别:
如果系统的订单号不是 UUID,需要额外生成一个 UUID 与订单号关联。
支付验证 + 异步回调
回调地址 只能在苹果后台配置,不能像微信/支付宝那样在每次下单时自由指定。
只能配置一个回调地址:
特性 | V1(旧版) | V2(新版) |
---|---|---|
凭证获取 | 客户端支付后获取 receipt (Base64) | 客户端支付后获取 transactionId |
验证方式 | 客户端上传 receipt 到服务端 → 服务端调用 Apple 接口验证 | 服务端通过 transactionId 调用 Apple Server API 查询订单 |
异步通知 | ❌ 不支持 | ✅ 支持,Apple 会推送 signedPayload |
环境区分 | production / sandbox | production / sandbox + Apple TestFlight |
幂等机制 | 需自行实现,难度大 | transactionId 唯一,全局可幂等 |
风险点 | 客户端可伪造请求 → 容易被破解 | 服务端直连 Apple 验证 + JWT 签名,安全性更高 |
? 可以看出,V2 的设计更标准化,和微信/支付宝非常接近。
整体步骤如下:
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
引入依赖(查看git地址,获取最新版本)
<dependency>
<groupId>com.apple.itunes.storekit</groupId>
<artifactId>app-store-server-library</artifactId>
<version>3.5.0</version>
</dependency>
git地址:github.com/apple/app-s…
注意:sdk的全部版本需要Java 11+
本次演示环境: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
订阅等,其中订阅也分一次性订阅,自动续订等,需要根据自己的实际业务,再结合官方文档,摸索清楚