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 退款与消费信息请求。调试时想看带签名的字符串,可以粘贴到 App Store JWS 解码器;本地测试购买,可以在 StoreKit 配置查看器 里打开你的 .storekit 文件。
设备上:VerificationResult
VerificationResult<SignedType> 是一个 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 说不要信任它,只用于调试。
交易的三个来源
- 购买调用。
product.purchase(options:)返回PurchaseResult:.success(带VerificationResult<Transaction>)、.pending(需要用户操作,例如“请求购买”;完成后会通过Transaction.updates到达)或.userCancelled。 Transaction.currentEntitlements,即用户当前有权益的每个商品的最新交易。Apple 列出的包括:每个非消耗型内购;每个续期订阅中续期状态为subscribed或inGracePeriod的最新交易;以及每个非续期订阅的最新交易(含已完成的)。已退款或被撤销的商品不会出现,消耗型商品永远不会出现(那类请用unfinished或all)。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():
import Foundation
import StoreKit
@MainActor
final class Store {
private(set) var unlocked: Set<String> = []
private var updatesTask: Task<Void, Never>?
// 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<String> = []
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<Transaction>) 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 编译并运行过(它会拒绝一个乱写的字符串;没有真实的签名交易,我们只能测到这一步):
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,及其 verified) 与 unverified) case、payloadValue、unsafePayloadValue 和 VerificationError,StoreKit
- currentEntitlements、updates、unfinished 与 finish()),StoreKit
- appAccountToken、environment、revocationDate、AppStore.Environment 与 isUpgraded,StoreKit
- purchase(options:)) 与 Product.PurchaseResult,StoreKit
- Simplifying your implementation by using the App Store Server Library,App Store Server API
- JWSTransaction、JWSTransactionDecodedPayload 与 JWSDecodedHeader,App Store Server API
- data(signedTransactionInfo),App Store Server Notifications
- Apple PKI,根证书
- app-store-server-library-swift,README 与
SignedDataVerifier.swift,Apple 的 GitHub
Apple 的页面和 API 会变化,请以上面各页的最新内容为准。