iOS内购场景下如何授权AWS Cognito用户,避免订阅共享?
问题根源
这个问题的核心是你的应用没有将Apple IAP订阅记录与AWS Cognito用户身份做绑定关联。iOS沙盒(正式环境同理)中,IAP购买凭证是绑定当前登录的Apple ID的,当同一设备切换Cognito用户时,如果应用直接读取本地缓存的订阅状态、或仅验证Apple ID的订阅而不校验用户身份,就会出现不同Cognito用户共享同一Apple ID订阅的情况。
解决办法
1. 购买成功后,绑定订阅凭证与Cognito用户
用户完成订阅购买后,不要仅在本地保存状态,需执行以下操作:
- 获取Apple返回的交易凭证(receipt data)
- 将凭证与当前登录的Cognito用户ID(可通过
AWSCognitoIdentityUserPool.default().currentUser()?.username或用户属性中的sub字段获取)一同上传至你的后端服务 - 后端调用Apple的凭证验证接口(沙盒环境用
https://sandbox.itunes.apple.com/verifyReceipt,正式环境用https://buy.itunes.apple.com/verifyReceipt)确认有效性,验证通过后,将订阅状态(有效期、订阅类型、续订状态等)与该Cognito用户ID关联存储(比如存入DynamoDB)
2. 用户登录时,从后端拉取专属订阅状态
当新用户(比如用户B)登录应用时:
- 禁止直接读取本地缓存的订阅信息,必须向你的后端发起请求,携带当前用户的Cognito ID
- 后端根据用户ID查询对应的专属订阅记录,返回该用户自身的订阅状态
- 应用完全依据后端返回的结果来解锁/限制订阅功能,不再依赖本地的Apple ID订阅状态
3. 处理订阅自动续订与状态同步
- 开启Apple的Server-to-Server Notifications(服务器通知),当订阅发生续订、过期、取消等状态变化时,Apple会主动向你的后端推送通知
- 后端收到通知后,立即更新对应Cognito用户的订阅状态,确保用户的订阅状态始终为最新
- 用户每次启动应用时,可主动向后端发起一次订阅状态同步请求,避免本地缓存的状态过期失效
4. 清理本地订阅缓存,避免跨用户串用
- 尽量不要在本地持久化订阅状态(比如
UserDefaults);如果必须使用本地缓存,在用户切换账号时,务必清空之前的订阅缓存数据,防止新用户读取到旧用户的订阅信息
关键代码示例(iOS端)
购买成功后上传凭证与用户ID
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { for transaction in transactions { switch transaction.transactionState { case .purchased: // 获取交易凭证 guard let receiptURL = Bundle.main.appStoreReceiptURL, let receiptData = try? Data(contentsOf: receiptURL) else { // 处理凭证获取失败逻辑 break } let receiptBase64 = receiptData.base64EncodedString() // 获取当前Cognito用户ID if let currentUser = AWSCognitoIdentityUserPool.default().currentUser() { currentUser.getDetails().continueWith { task in guard let userAttributes = task.result?.userAttributes else { return nil } let userId = userAttributes["sub"] ?? currentUser.username // 上传凭证与用户ID到后端 self.uploadReceiptToBackend(receipt: receiptBase64, userId: userId) return nil } } // 务必完成交易 SKPaymentQueue.default().finishTransaction(transaction) // 处理其他交易状态(如失败、恢复等) default: break } } }
用户登录后拉取专属订阅状态
func fetchUserSubscriptionStatus(userId: String) { guard let url = URL(string: "https://your-backend-domain/subscription/status") else { return } var request = URLRequest(url: url) request.httpMethod = "POST" request.httpBody = try? JSONSerialization.data(withJSONObject: ["userId": userId]) request.setValue("application/json", forHTTPHeaderField: "Content-Type") URLSession.shared.dataTask(with: request) { data, _, error in guard let data = data, error == nil else { return } do { let status = try JSONDecoder().decode(SubscriptionStatus.self, from: data) DispatchQueue.main.async { // 根据后端返回的状态更新UI或功能权限 self.updateSubscriptionAccess(with: status) } } catch { // 解析失败处理 } }.resume() } // 自定义订阅状态模型 struct SubscriptionStatus: Codable { let isActive: Bool let expirationDate: Date? let subscriptionType: String }
内容的提问来源于stack exchange,提问作者Rino
相关产品推荐
相关产品推荐

