StoreKit 1 vs StoreKit 2 深度技术对比报告
1. 执行摘要
随着 iOS 生态系统的成熟,StoreKit 框架经历了从基于 Objective-C 观察者模式的 StoreKit 1 (Original API) 到基于 Swift 结构化并发的 StoreKit 2 (Modern API) 的重大范式转变。
StoreKit 1 虽久经考验,但存在集成复杂、收据格式(ASN.1)晦涩、依赖本地 OpenSSL 校验等痛点。StoreKit 2 利用 Swift 5.5+ 的 async/await 特性简化了交易流程,引入了基于行业标准 JWS (JSON Web Signature) 的验证机制,并提供了更加透明、安全的 Transaction 对象模型。
本报告将详细对比两代框架在架构、代码实现、收据验证及安全性上的差异,并重点剖析 StoreKit 1 收据体积膨胀的根本原因及 StoreKit 2 JWS 的底层实现。
2. 架构演进:从观察者模式到结构化并发
2.1 StoreKit 1:观察者模式与全局队列
StoreKit 1 的核心是 SKPaymentQueue。开发者必须实现 SKPaymentTransactionObserver 协议。
- 控制流断裂: 购买请求与结果回调在不同的函数中,状态管理依赖开发者手动维护。
- 生命周期风险: 若 App 在交易回调前崩溃,交易会滞留。开发者必须在启动时尽早添加观察者以处理
unfinished transactions,否则会导致漏单。
2.2 StoreKit 2:结构化并发 (Swift Concurrency)
StoreKit 2 采用线性控制流。Product 和 Transaction 变为结构体(Struct)。
- 线性化调用:
await product.purchase()直接返回购买结果,无需跨函数传递状态。 - 被动状态同步: 使用
Transaction.updates异步序列(AsyncSequence)处理应用外的交易(如续费),替代了复杂的 Delegate 回调。
2.3 核心架构差异对比表
| 特性维度 | StoreKit 1 (Original API) | StoreKit 2 (Modern Swift API) |
|---|---|---|
| 设计模式 | 委托/观察者模式 (Delegate/Observer) | 结构化并发 (Async/Await) |
| 核心对象 | 类 (SKProduct, SKPaymentTransaction) |
结构体 (Product, Transaction) |
| 购买流程 | 添加到队列 -> 等待回调 -> 手动完成 | 调用函数 -> await 返回值 -> 自动验证 -> 手动完成 |
| 收据/凭证 | 统一的 App Receipt (ASN.1 二进制文件) | 离散的 JWS 交易对象 |
| 恢复购买 | restoreCompletedTransactions() (需密码) |
Transaction.currentEntitlements (静默、自动) |
3. 编程范式对比:代码层面的变革
3.1 StoreKit 1 购买代码
// 逻辑分散在多个方法中
class IAPManager: NSObject, SKPaymentTransactionObserver {
func buy(product: SKProduct) {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions:) {
for transaction in transactions {
switch transaction.transactionState {
case.purchased:
complete(transaction) // 验证收据逻辑
case.failed:
fail(transaction)
//... 处理其他状态
}
}
}
}
3.2 StoreKit 2 购买代码
// 逻辑线性化,更加直观
func purchase(product: Product) async {
do {
let result = try await product.purchase()
switch result {
case.success(let verification):
// 系统自动验证签名
switch verification {
case.verified(let transaction):
await transaction.finish() // 完成交易
deliverContent()
case.unverified:
print("签名验证失败")
}
case.userCancelled:
break
case.pending:
break
}
} catch {
print("购买出错: \(error)")
}
}
4. 收据校验体系的根本性重构
收据校验是 IAP 安全的核心。这是两个框架差异最剧烈、技术跨度最大的领域。
4.1 StoreKit 1: 统一应用收据 (App Receipt)
StoreKit 1 依赖位于 Bundle.main.appStoreReceiptURL 的单一二进制文件。这是一个 PKCS #7 容器,内部封装了 ASN.1 格式的数据。
verifyReceipt HTTP 接口,这带来了网络延迟和单点故障风险。
4.2 StoreKit 2: 交易即凭证
StoreKit 2 废弃了“统一收据文件”。每一笔交易(Transaction)都是一个独立的、自包含的 JWS 对象。系统会在底层自动进行密码学验证,开发者拿到的 Transaction 对象通常已经是 Verified 状态。
5. 深度剖析:StoreKit 1 收据结构与体积分析
在 StoreKit 1 中,开发者经常发现收据文件(Receipt File)体积庞大(数 KB 到数 MB)。这并非乱码,而是由其特殊的设计架构决定的。
5.1 为什么 SK1 收据文件这么大?
主要有三大原因:
- PKCS #7 容器与证书链 (Certificate Chain): 收据不仅仅是数据,它是一个加密容器。为了支持离线验证,Apple 在收据文件中嵌入了完整的证书链(包括 Apple Root CA、WWDR Intermediate Certificate 等)。仅证书部分就占用了约 4KB - 6KB 的空间。
- ASN.1 DER 编码冗余: 收据内容采用 ASN.1 标准的 DER 编码。这种 TLV (Type-Length-Value) 结构非常严谨但冗余度高。存储一个简单的整数可能需要额外的 10 字节“包装”。
- 历史账本机制 (Append-Only):
- 对于自动续期订阅,每次续费都会生成一个新的 Type 17 记录并追加到收据中,旧记录不会被删除。
- 这意味着一个订阅了 5 年的用户,收据里会包含 60+ 条历史记录。
- 在 Sandbox 测试环境下,订阅周期极短(5分钟),测试账号极易产生包含数百条记录的 MB 级大收据。
5.2 收据内部包含什么?(ASN.1 字段详解)
解析 PKCS #7 容器后,Payload 包含以下关键字段:
- Type 2 (Bundle ID): 应用包名,用于防止收据注入(将 A 应用的收据给 B 应用用)。
- Type 3 (App Version): 购买时的应用版本。
- Type 19 (Original App Version): 用户首次下载应用的版本(用于老用户权益判定)。
- Type 12 (Creation Date): 收据生成时间。
- Type 4 (Opaque Value) & Type 5 (SHA-1 Hash):
本地验证的核心。SHA-1(Device_GUID + Opaque + Bundle_ID)必须等于 Type 5 的值。这确保了收据是属于当前设备的,不能通过 AirDrop 发给别人用。
这是一个数组,包含多条购买记录。每条记录内部又是一个 ASN.1 Set,包含:
- Type 1701 (Quantity): 数量。
- Type 1702 (Product ID): 商品 ID。
- Type 1703 (Transaction ID): 交易唯一流水号。
- Type 1705 (Original Transaction ID): 订阅链的根 ID,用于关联所有续费记录。
- Type 1704 (Purchase Date): 购买时间。
- Type 1708 (Expires Date): 订阅过期时间(仅订阅有)。
6. 客户端与服务端交互流程对比
6.1 StoreKit 1 流程 (依赖 Apple 验证)
- Client: 监听
updatedTransactions收到购买成功回调。 - Client: 读取本地收据
Bundle.main.appStoreReceiptURL。 - Client: 将收据进行 Base64 编码,发送给 Server。
- Server: 将收据转发给 Apple 的
verifyReceipt接口 (先 Production 后 Sandbox)。 - Server: 解析 Apple 返回的 JSON,处理业务逻辑。
- Server: 返回成功给 Client。
- Client: 调用
finishTransaction。
6.2 StoreKit 2 流程 (JWS 离线验证)
- Client:
await product.purchase()返回VerificationResult。 - Client: 提取
jwsRepresentation字符串,发送给 Server。 - Server: 本地验证签名 (使用 Apple Root CA 公钥解密 JWS),无需请求 Apple 服务器。
- Server: 解析 JWS Payload,处理业务逻辑。
- Server: 返回成功给 Client。
- Client: 调用
await transaction.finish()。
7. 核心深度剖析:StoreKit 2 的 JWS
StoreKit 2 使用符合 RFC 7515 标准的 JSON Web Signature。格式为 Header.Payload.Signature。
7.1 JWS Header
{
"alg": "ES256", // 算法:ECDSA P-256 + SHA-256
"x5c": ["MII...",...] // 证书链 (Base64)
}
7.2 JWS Payload (业务数据)
解码后包含清晰的 JSON 数据,无需像 SK1 那样处理 ASN.1。
transactionId: 交易 IDoriginalTransactionId: 原始交易 IDbundleId: 包名expiresDate: 过期时间戳appAccountToken: 新特性,用于绑定开发者用户系统的 UserID (UUID格式)。
7.3 验证原理
服务端只需:1. 验证 x5c 证书链根节点是 Apple Root CA G3;2. 使用证书公钥验证 Signature。无需任何网络请求即可确认交易合法性。
8. 安全性与隐私
- 中间人攻击 (MITM): SK1 的
verifyReceipt易受 DNS 劫持攻击。SK2 的 JWS 是自签名的,篡改内容会导致签名失效,彻底杜绝了此类攻击。 - 设备绑定: SK2 的 JWS 包含
deviceVerification字段,防止 JWS 被复制到其他设备重放。
9. 兼容性与迁移策略
对于需要支持旧版本的 App,建议采用混合架构 (Hybrid Strategy):
- iOS 15+ 用户:使用 StoreKit 2 API。
- iOS 14- 用户:保留 StoreKit 1 代码。
迁移优势: StoreKit 2 会自动读取 SK1 的旧收据并转换为新的 JWS 格式,老用户升级 App 后无缝过渡。
10. 结论
StoreKit 2 不仅仅是一次 API 更新,而是底层信任机制的重构。它通过 JWS 解决了 StoreKit 1 收据体积大、解析难、验证慢的问题。
建议:
- 新项目: 全面拥抱 StoreKit 2。
- 老项目: 在服务端引入 JWS 验证能力,客户端逐步在 iOS 15+ 设备上启用 StoreKit 2 路径,利用其
unfinished序列更可靠地解决漏单问题。