# 验证 StoreKit 2 交易 > VerificationResult、当前权益、Transaction.updates 监听、finish()、appAccountToken、退款撤销与环境,以及在服务器上校验带签名的 JWS。 - Canonical page: https://obelisk.club/blog/storekit-2-transaction-verification-zh - Author: Peter Zhang - Published: 2026-11-01 - Tags: storekit, in-app-purchase, app-store-server-api, subscriptions, swift, ios, indie, 中文 StoreKit 2 把每笔交易都包在一个 `VerificationResult` 里。只根据 `.verified` 解锁内容,不要根据 `.unverified`;App 启动时就开始 `Transaction.updates` 循环,别漏掉在别处完成的购买;用 `Transaction.currentEntitlements` 了解用户现在拥有什么;只有在真正交付了用户付费的内容之后,才调用 `finish()`。Apple 说 StoreKit 会检查它交给你的值;想要更强的控制,可以用 `jwsRepresentation` 里的 JWS 字符串,在设备上或服务器上自己验证。在服务器上,Apple 推荐使用它的 App Store Server Library,它会替你检查签名、证书链、Bundle ID 和环境。处理一笔交易之前,还要看它的 `revocationDate`、`appAccountToken` 和 `environment`。 这篇讲验证。以服务器通知形式到达的退款,请看 [App Store 退款与消费信息请求](/blog/app-store-refunds-consumption-requests)。调试时想看带签名的字符串,可以粘贴到 [App Store JWS 解码器](/tools/app-store-jws-decoder);本地测试购买,可以在 [StoreKit 配置查看器](/tools/storekit-config-viewer) 里打开你的 `.storekit` 文件。 ## 设备上:VerificationResult `VerificationResult` 是一个 frozen 枚举,有两个 case: - `.verified(SignedType)`:值通过了 StoreKit 的检查。关联值是 App Store 签名的值。 - `.unverified(SignedType, VerificationError)`:值没通过检查。你仍能拿到签名的值,另外附带失败原因。 Apple 的 `VerificationError` 有这些 case:`invalidCertificateChain`(链中有证书过期或不受信任)、`invalidDeviceVerification`(签名的值不是为这台设备生成的)、`invalidEncoding`、`invalidSignature`(签名与头部和负载不匹配)、`missingRequiredProperties`,以及 `revokedCertificate`(Apple 说有时稍后重试就能解决)。 `.unverified` 怎么处理由你决定;Apple 的示例写的是“按你的商业模式处理未验证的交易”。对付费解锁来说,稳妥的做法是什么都不授予并记录错误。对未验证的值访问 `payloadValue` 会抛出错误,而 `unsafePayloadValue` 不管结果如何都会返回数据;Apple 说不要信任它,只用于调试。 ## 交易的三个来源 1. **购买调用。** `product.purchase(options:)` 返回 `PurchaseResult`:`.success`(带 `VerificationResult`)、`.pending`(需要用户操作,例如“请求购买”;完成后会通过 `Transaction.updates` 到达)或 `.userCancelled`。 2. **`Transaction.currentEntitlements`**,即用户当前有权益的每个商品的最新交易。Apple 列出的包括:每个非消耗型内购;每个续期订阅中续期状态为 `subscribed` 或 `inGracePeriod` 的最新交易;以及每个非续期订阅的最新交易(含已完成的)。已退款或被撤销的商品不会出现,消耗型商品永远不会出现(那类请用 `unfinished` 或 `all`)。 3. **`Transaction.updates`**,用于购买调用之外发生的一切:“请求购买”获批、优惠码兑换、在 App Store 里完成的购买,以及在另一台设备上完成的购买。Apple 说应在 App 启动时就创建一个 `Task` 来遍历它,因为未完成的交易只会在启动后立刻投递一次,“没有这个 Task 来监听,你的 App 可能会错过它们”。 ## finish() `finish()` 告诉 App Store 你已交付内容或开通了服务。Apple 的说法是:在交付购买的内容或开通购买的服务之后调用;对于按需资源,要等下载完成。在你调用之前,交易一直留在 `Transaction.unfinished` 里。请在更新完自己的状态之后再调 `finish()`,不要在之前,这样中途崩溃也不会丢掉用户已付费的购买。 ## 完整示例 下面这段代码用 iOS 18 SDK 和 Swift 6.4 编译通过(Swift 6 语言模式下也通过)。`@MainActor` 类在 `init` 里启动监听,购买时带上 `appAccountToken`,用权益重建已解锁集合,并且只对通过验证的交易调用 `finish()`: ```swift import Foundation import StoreKit @MainActor final class Store { private(set) var unlocked: Set = [] private var updatesTask: Task? // Start this as soon as the app launches (for example in init of your app model). init() { updatesTask = Task { [weak self] in for await result in Transaction.updates { await self?.handle(result) } } } deinit { updatesTask?.cancel() } func buy(_ product: Product, accountToken: UUID) async throws { let result = try await product.purchase(options: [.appAccountToken(accountToken)]) switch result { case .success(let verification): await handle(verification) case .pending, .userCancelled: break @unknown default: break } } func refreshEntitlements() async { var ids: Set = [] for await result in Transaction.currentEntitlements { if case .verified(let transaction) = result, transaction.revocationDate == nil { ids.insert(transaction.productID) } } unlocked = ids } private func handle(_ result: VerificationResult) async { switch result { case .unverified(_, let error): // Do not unlock. Log the reason. print("unverified:", error) case .verified(let transaction): if transaction.revocationDate != nil { unlocked.remove(transaction.productID) } else { unlocked.insert(transaction.productID) } // Only after the content is delivered: await transaction.finish() } } } ``` 我们只做了类型检查,没有运行,因为购买需要 StoreKit 配置文件或沙盒账号。Apple 对 `purchase(options:)` 的说明建议:SwiftUI 用 `PurchaseAction`,UIKit 和 AppKit 用 `purchase(confirmIn:options:)`;界面里请用它们,处理逻辑沿用上面的写法。 ## 每笔交易要检查什么 | 字段 | Apple 的说明 | 怎么做 | |---|---|---| | `revocationDate` | App Store 退款或把交易从“家人共享”中撤销的日期。 | 一旦有值,就收回权限。`currentEntitlements` 已经把这类交易排除在外。 | | `appAccountToken` | 你在购买选项里设置的 UUID,App Store 会在交易里原样返回。你没提供就不会有这个字段。 | 在服务器上和当前登录用户比对,避免交易被挂到别人的账号上。 | | `environment` | 生成并签名这笔交易的服务器环境:设备上是 `.production`、`.sandbox` 或 `.xcode`;服务器数据里是 `Production` 或 `Sandbox`。 | 让服务器拒绝与自己预期环境不一致的数据。 | | `expirationDate` 与 `isUpgraded` | 订阅到期或续期的日期;用户升级到另一个订阅时 `isUpgraded` 为 true。 | 不要根据已过期或已升级的交易授予权限;Apple 自己的示例两种都会跳过。 | | `productID` | 你在 App Store Connect 里设定的商品标识。 | 与你要解锁的内容对应起来。 | 如果要给在 App 之外完成的购买设置或修改 app account token,Apple 的服务器 API 有 Set App Account Token 接口。 ## 服务器上:自己校验 JWS 带签名的字符串是 Compact Serialization 格式的 JWS:头部、负载、签名三段 Base64URL,用点连接。它与 App Store Server API 和 App Store Server Notifications V2 里的 `JWSTransaction` 格式相同;在通知里它位于 `data` 内的 `signedTransactionInfo`(续期信息是 `signedRenewalInfo`)。设备上的负载还有 `deviceVerification` 和 `deviceVerificationNonce` 字段,用来确认这个值属于该设备。 Apple 对头部的说明:`alg` 是签名算法,`x5c` 是证书链,顺序为:持有签名该 JWS 的密钥的证书、一张 Apple 中间证书(带有 Worldwide Developer Relations 扩展 ID 1.2.840.113635.100.6.2.1),以及一张 Apple 根证书。你信任的根证书必须是你自己从 Apple 下载的,而不是消息里带的那张:Apple PKI 网站列有 Apple Root CA - G3,我们的解码器就是用它来检查证书链的。我们的解码器还要求 `alg` 为 `ES256`。 这些检查你可以自己写,但 Apple 推荐使用它的开源库 App Store Server Library,有 Swift、Java、Python 和 Node 版本,提供 `verifyAndDecodeTransaction`、`verifyAndDecodeAppTransaction` 和 `verifyAndDecodeRenewalInfo` 函数。Swift 版里,`SignedDataVerifier` 需要 Apple 根证书、你的 Bundle ID、你的数字 App ID(生产环境必填)和环境;如果交易里的 Bundle ID 或环境对不上,它会拒绝。`enableOnlineChecks` 选项会开启吊销检查,并按当前日期检查是否过期。 下面这段代码基于 App Store Server Library 6.0.0 编译并运行过(它会拒绝一个乱写的字符串;没有真实的签名交易,我们只能测到这一步): ```swift import Foundation import AppStoreServerLibrary func verifiedTransaction( signedTransaction: String, appleRootCertificates: [Data], bundleId: String, appAppleId: Int64?, environment: AppStoreEnvironment ) async throws -> JWSTransactionDecodedPayload? { let verifier = try SignedDataVerifier( rootCertificates: appleRootCertificates, bundleId: bundleId, appAppleId: appAppleId, environment: environment, enableOnlineChecks: true ) switch await verifier.verifyAndDecodeTransaction(signedTransaction: signedTransaction) { case .valid(let payload): return payload case .invalid: return nil } } func shouldGrantAccess(_ t: JWSTransactionDecodedPayload, expectedUser: UUID) -> Bool { guard t.appAccountToken == expectedUser else { return false } return t.revocationDate == nil } ``` 在设备上,把 `verification.jwsRepresentation` 连同用户的会话一起发给服务器。在服务器上,先验证,再比对 `appAccountToken` 和用户,然后才执行操作。续期、退款这类长期状态,请依靠通知,不要只信 App 上报的内容。 ## 来源 均于 2026 年 11 月 1 日查阅。 - [VerificationResult](https://developer.apple.com/documentation/storekit/verificationresult),及其 [verified](https://developer.apple.com/documentation/storekit/verificationresult/verified(_:)) 与 [unverified](https://developer.apple.com/documentation/storekit/verificationresult/unverified(_:_:)) case、[payloadValue](https://developer.apple.com/documentation/storekit/verificationresult/payloadvalue)、[unsafePayloadValue](https://developer.apple.com/documentation/storekit/verificationresult/unsafepayloadvalue) 和 [VerificationError](https://developer.apple.com/documentation/storekit/verificationresult/verificationerror),StoreKit - [currentEntitlements](https://developer.apple.com/documentation/storekit/transaction/currententitlements)、[updates](https://developer.apple.com/documentation/storekit/transaction/updates)、[unfinished](https://developer.apple.com/documentation/storekit/transaction/unfinished) 与 [finish()](https://developer.apple.com/documentation/storekit/transaction/finish()),StoreKit - [appAccountToken](https://developer.apple.com/documentation/storekit/transaction/appaccounttoken)、[environment](https://developer.apple.com/documentation/storekit/transaction/environment)、[revocationDate](https://developer.apple.com/documentation/storekit/transaction/revocationdate)、[AppStore.Environment](https://developer.apple.com/documentation/storekit/appstore/environment) 与 [isUpgraded](https://developer.apple.com/documentation/storekit/transaction/isupgraded),StoreKit - [purchase(options:)](https://developer.apple.com/documentation/storekit/product/purchase(options:)) 与 [Product.PurchaseResult](https://developer.apple.com/documentation/storekit/product/purchaseresult),StoreKit - [Simplifying your implementation by using the App Store Server Library](https://developer.apple.com/documentation/appstoreserverapi/simplifying-your-implementation-by-using-the-app-store-server-library),App Store Server API - [JWSTransaction](https://developer.apple.com/documentation/appstoreserverapi/jwstransaction)、[JWSTransactionDecodedPayload](https://developer.apple.com/documentation/appstoreserverapi/jwstransactiondecodedpayload) 与 [JWSDecodedHeader](https://developer.apple.com/documentation/appstoreserverapi/jwsdecodedheader),App Store Server API - [data](https://developer.apple.com/documentation/appstoreservernotifications/data)(signedTransactionInfo),App Store Server Notifications - [Apple PKI](https://www.apple.com/certificateauthority/),根证书 - [app-store-server-library-swift](https://github.com/apple/app-store-server-library-swift),README 与 `SignedDataVerifier.swift`,Apple 的 GitHub *Apple 的页面和 API 会变化,请以上面各页的最新内容为准。*