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 协议。

2.2 StoreKit 2:结构化并发 (Swift Concurrency)

StoreKit 2 采用线性控制流。ProductTransaction 变为结构体(Struct)。

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 格式的数据。

SK1 痛点: 本地解析 ASN.1 极其复杂,通常需要引入 OpenSSL。因此,大多数开发者不得不依赖 Apple 的 verifyReceipt HTTP 接口,这带来了网络延迟和单点故障风险。

4.2 StoreKit 2: 交易即凭证

StoreKit 2 废弃了“统一收据文件”。每一笔交易(Transaction)都是一个独立的、自包含的 JWS 对象。系统会在底层自动进行密码学验证,开发者拿到的 Transaction 对象通常已经是 Verified 状态。

5. 深度剖析:StoreKit 1 收据结构与体积分析

在 StoreKit 1 中,开发者经常发现收据文件(Receipt File)体积庞大(数 KB 到数 MB)。这并非乱码,而是由其特殊的设计架构决定的。

5.1 为什么 SK1 收据文件这么大?

主要有三大原因:

  1. PKCS #7 容器与证书链 (Certificate Chain): 收据不仅仅是数据,它是一个加密容器。为了支持离线验证,Apple 在收据文件中嵌入了完整的证书链(包括 Apple Root CA、WWDR Intermediate Certificate 等)。仅证书部分就占用了约 4KB - 6KB 的空间。
  2. ASN.1 DER 编码冗余: 收据内容采用 ASN.1 标准的 DER 编码。这种 TLV (Type-Length-Value) 结构非常严谨但冗余度高。存储一个简单的整数可能需要额外的 10 字节“包装”。
  3. 历史账本机制 (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 发给别人用。
Type 17: 应用内购买记录 (In-App Purchase Receipt)

这是一个数组,包含多条购买记录。每条记录内部又是一个 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 验证)

  1. Client: 监听 updatedTransactions 收到购买成功回调。
  2. Client: 读取本地收据 Bundle.main.appStoreReceiptURL
  3. Client: 将收据进行 Base64 编码,发送给 Server。
  4. Server: 将收据转发给 Apple 的 verifyReceipt 接口 (先 Production 后 Sandbox)。
  5. Server: 解析 Apple 返回的 JSON,处理业务逻辑。
  6. Server: 返回成功给 Client。
  7. Client: 调用 finishTransaction

6.2 StoreKit 2 流程 (JWS 离线验证)

  1. Client: await product.purchase() 返回 VerificationResult
  2. Client: 提取 jwsRepresentation 字符串,发送给 Server。
  3. Server: 本地验证签名 (使用 Apple Root CA 公钥解密 JWS),无需请求 Apple 服务器。
  4. Server: 解析 JWS Payload,处理业务逻辑。
  5. Server: 返回成功给 Client。
  6. 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。

7.3 验证原理

服务端只需:1. 验证 x5c 证书链根节点是 Apple Root CA G3;2. 使用证书公钥验证 Signature。无需任何网络请求即可确认交易合法性。

8. 安全性与隐私

9. 兼容性与迁移策略

系统要求: StoreKit 2 需要 iOS 15+

对于需要支持旧版本的 App,建议采用混合架构 (Hybrid Strategy)

迁移优势: StoreKit 2 会自动读取 SK1 的旧收据并转换为新的 JWS 格式,老用户升级 App 后无缝过渡。

10. 结论

StoreKit 2 不仅仅是一次 API 更新,而是底层信任机制的重构。它通过 JWS 解决了 StoreKit 1 收据体积大、解析难、验证慢的问题。

建议: