iOS 应用内购买:StoreKit 与收据验证
什么是应用内购买
应用内购买(In-App Purchase,简称 IAP)是 iOS 应用在 App Store 内销售数字内容与服务的核心机制。你可以通过它在应用中提供订阅、解锁高级功能、出售虚拟商品或移除广告。所有使用 IAP 的交易都必须通过 Apple 的 StoreKit 框架处理,并且需要配合服务端收据验证来保证安全性。本教程将带你从零开始,实现一个完整的应用内购买流程,并搭建可靠的收据验证。
环境准备与项目配置
在编写代码之前,你需要完成以下准备工作:
- 一个已加入 Apple Developer Program 的 Apple ID。
- 在 Xcode 中创建项目,并设置好 Bundle Identifier。
- 在 App Store Connect 中为你的 App 创建对应的 App 记录。
在 App Store Connect 中创建购买项目
登录 App Store Connect,进入你的 App 页面,点击 功能 标签下的 App 内购买项目。点击加号,选择你要销售的类型:
- 消耗型:每次购买都会消耗,例如游戏币、体力值。用户可以多次购买同一商品。
- 非消耗型:一次性购买,永久拥有。例如移除广告、解锁完整版功能。
- 自动续期订阅:按周期扣费并自动续订,例如月度会员。
- 非续期订阅:有时限的服务访问,结束后不自动续费,需要用户再次购买。
填写参考名称(仅用于后台显示)、产品 ID(代码中使用的唯一标识)、定价等必要信息,并将状态设为“准备提交”。记下你设置的产品 ID,稍后会在代码中使用。
使用 StoreKit 实现购买流程
StoreKit 是处理 IAP 的原生框架。在 iOS 15 之后,Apple 引入了基于 Swift 并发的新 API(StoreKit 2),它更简洁安全。本教程将展示 StoreKit 2 的实现方式,如果你的 App 需要支持 iOS 14 及更低版本,再回退到原 StoreKit 框架。
导入 StoreKit 并请求产品信息
在你的购买管理类中,首先导入 StoreKit,然后根据你在 App Store Connect 中设置的产品 ID 数组,请求产品列表。
import StoreKit
class StoreManager: ObservableObject {
static let productIDs = ["com.yourapp.premium", "com.yourapp.coins.100"]
@Published var products: [Product] = []
func requestProducts() async {
do {
let storeProducts = try await Product.products(for: Self.productIDs)
await MainActor.run {
self.products = storeProducts
}
} catch {
print("加载产品失败: \(error.localizedDescription)")
}
}
}
Product.products(for:) 返回一个 Product 数组。每个 Product 都包含了在店内显示的本地化信息:价格、名称、描述等。你可以直接用它们构建购买界面。
构建购买按钮并处理交易
在 SwiftUI 视图中,列出产品并使用 AsyncButton 或手动触发购买。以下是购买动作的关键代码:
func purchase(_ product: Product) async {
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
// 验证交易并完成
let transaction = try checkVerified(verification)
await transaction.finish()
// 这里可以解锁内容,并调用服务端收据验证
await deliverProduct(for: transaction)
case .userCancelled:
break
case .pending:
// 例如家长审批等待中
break
@unknown default:
break
}
} catch {
print("购买失败: \(error.localizedDescription)")
}
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .verified(let safe):
return safe
case .unverified:
throw StoreError.failedVerification
}
}
product.purchase() 会触发系统购买界面。成功后你会得到一个 VerificationResult<Transaction>,它由 StoreKit 自动生成并签名。务必调用 checkVerified 检查数据是否真实可信,然后调用 transaction.finish() 告诉 StoreKit 交易已完成。如果不调用 finish(),这笔交易会一直处于未完成状态,并在一段时间后可能导致重复弹出购买成功提示。
观察已完成的交易和订阅状态
为了处理在 App 外完成的交易(例如家长审批后),以及恢复之前的购买记录,你需要持续监听交易更新。在 iOS 15 以上,可以通过监听 Transaction.updates 异步序列来实现:
func observeTransactionUpdates() async {
for await result in Transaction.updates {
do {
let transaction = try checkVerified(result)
await deliverProduct(for: transaction)
await transaction.finish()
} catch {
print("交易验证失败")
}
}
}
恢复购买可以直接使用 AppStore.sync() 来重新同步用户的所有交易。
收据验证的必要性与原理
StoreKit 的客户端验证只能确保数据在设备与 Apple 服务器之间未篡改,但攻击者可能绕过客户端验证,直接使用伪造的返回结果。因此,任何严肃的应用内购买实现都需要设置服务端收据验证(Server-Side Receipt Validation),在你的服务器上向 Apple 的验证接口发起请求,再由服务器返回结果给客户端。
收据(receipt)是描述购买数字商品的记录,包含交易 ID、产品 ID、购买时间、到期时间等信息。每次成功交易或订阅续期时,系统都会自动生成新的收据,并更新到设备上。
获取收据数据
在 StoreKit 2 中,所有历史交易都存储在 Transaction 对象中,你可以直接从 Transaction.currentEntitlements 获取当前有效的购买,而不需要再手动解析收据二进制文件。但若需完整的原始收据(例如某些订阅粒度较细的场景),仍可使用旧的 appStoreReceiptURL 方式获取收据。
服务端验证流程
- 客户端获取收据:调用
Transaction.currentEntitlements提取有效授权,或者通过AppStore.sync()获取最新状态。 - 将收据发送到你的服务器:客户端把交易信息或原始收据数据发送给你自己的后端。
- 服务端向 Apple 验证:后端使用收到的凭据向 Apple 的验证 URL(
https://buy.itunes.apple.com/verifyReceipt或沙盒环境https://sandbox.itunes.apple.com/verifyReceipt)发起 POST 请求,JSON 中包含receipt-data和password(用于订阅的共享密钥)。 - 解析响应:Apple 返回一个 JSON,其中
status为 0 表示成功,并且包含receipt信息。你需要检查receipt.in_app数组中对应的交易信息是否有效,尤其是订阅的expires_date和is_trial_period等字段。 - 向客户端返回授权结果:服务端确认购买有效后,将对应权限同步给客户端,并记录到数据库。
服务端代码示例(Python Flask)
此处提供一个简化的服务端验证示例,方便你理解逻辑。使用 Flask 框架,接收客户端发来的原始收据数据。
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
SHARED_SECRET = "你的共享密钥"
VERIFY_URL = "https://buy.itunes.apple.com/verifyReceipt"
SANDBOX_URL = "https://sandbox.itunes.apple.com/verifyReceipt"
@app.route("/verify", methods=["POST"])
def verify_receipt():
data = request.get_json()
receipt_data = data.get("receipt_data")
payload = {
"receipt-data": receipt_data,
"password": SHARED_SECRET,
"exclude-old-transactions": True
}
# 先尝试生产环境
response = requests.post(VERIFY_URL, json=payload)
result = response.json()
# 如果状态码为21007,代表需要调用沙盒环境
if result.get("status") == 21007:
response = requests.post(SANDBOX_URL, json=payload)
result = response.json()
if result.get("status") == 0:
# 验证通过,解析 receipt 字段,处理订阅或非消耗型商品逻辑
return jsonify({"valid": True, "receipt": result["receipt"]})
else:
return jsonify({"valid": False, "error": "收据无效"})
在实际生产中,你应该还加上对 receipt.in_app 内每一项的验证:确认 product_id 与期望一致,检查购买时间是否合理,防止重放攻击等。
处理订阅状态与自动续期
对于自动续期订阅,你需要在服务端定期轮询或使用**服务端通知(App Store Server Notifications)**来接收状态变化。Apple 提供了 V2 版本的通知,可以在订阅扣款成功、即将到期、续期失败等时机向你的服务器发送 JSON 事件。
配置方法:进入 App Store Connect > 你的 App > 通用 > App Store 服务器通知,设置你的服务器 URL。当事件发生时 Apple 会向其发送 POST 请求。你需要按官方文档解析 signedPayload JWS 格式的数据,其中包含事件类型和最新的订阅信息。
常见问题与最佳实践
- 沙盒测试:在沙盒环境中,订阅会加速到期时间,方便快速测试续费逻辑。请务必使用沙盒账号测试,不要使用生产 Apple ID。
- 用户退款:当用户申请退款成功,Apple 可能发送
REFUND类型的通知,或者通过状态更新告知你,务必在你的服务端处理权限回收。 - 防收据重放:客户端发往服务端的收据,服务端应记录交易 ID,确保同一交易 ID 不会被重复使用。
- 避免主 UI 线程卡顿:
Product.products(for:)和purchase()都是异步方法,不要在同步队列中阻塞等待。 - 本地化提示:利用
Product.displayName和Product.description展示系统原生购买表单,可以极大提升用户体验。
总结与下一步
至此,你已经掌握了在 iOS 应用中集成应用内购买的核心步骤:从 App Store Connect 配置产品,到使用 StoreKit 2 实现购买流程,再到搭建服务端收据验证。安全的收据验证是商业化 App 的基石,而 StoreKit 的新 API 让这一切变得更加简单可靠。
接下来,你可以深入阅读 Apple 官方文档《StoreKit》和《In-App Purchase》,以及实现订阅状态服务器通知,让你的购买系统更加健壮。