Flutter应用内购买验证方法及PurchaseDetails属性使用咨询
Flutter 内购购买验证实操(基于PurchaseDetails对象)
首先明确:只有当PurchaseDetails.status为PurchaseStatus.purchased或PurchaseStatus.restored时,才需要执行验证逻辑,pending/failed状态直接跳过。下面结合你提到的属性,分平台讲具体验证步骤:
核心属性对应用途
status:先过滤有效购买状态,是验证的前提purchaseID:对应iOS的transaction_id、Android的orderId,用于唯一标识交易,防止重复发放权益productID:验证购买的产品是否和用户实际购买的一致verificationData:包含平台专属的验证凭证(iOS是收据,Android是购买令牌+签名)transactionDate:验证交易时间是否合理(比如不能是未来时间,和平台返回的时间匹配)
iOS 端验证
iOS的verificationData.localVerificationData是App Store返回的Base64格式收据,serverVerificationData为空。推荐优先用服务器端验证,本地验证仅用于快速调试。
服务器端验证(生产环境必用)
- 把
purchaseDetails.verificationData.localVerificationData(收据字符串)发送到你的后端 - 后端向Apple验证接口发送POST请求:
- 沙盒环境:
https://sandbox.itunes.apple.com/verifyReceipt - 生产环境:
https://buy.itunes.apple.com/verifyReceipt
- 沙盒环境:
- 后端解析Apple返回的响应,重点验证:
- 响应中的
status字段为0(验证通过) receipt.in_app中的product_id和purchaseDetails.productID一致receipt.in_app中的transaction_id和purchaseDetails.purchaseID一致- 交易时间(
purchase_date_ms)和purchaseDetails.transactionDate匹配,且不是未来时间
- 响应中的
- 后端返回验证结果给Flutter端,通过则发放权益
本地验证(仅调试用)
import 'dart:convert'; import 'package:flutter/services.dart'; void verifyIOSPurchase(PurchaseDetails purchase) { if (purchase.status != PurchaseStatus.purchased && purchase.status != PurchaseStatus.restored) return; final receipt = purchase.verificationData.localVerificationData; try { // 解析Base64收据(实际需处理ASN.1格式,可借助第三方库) final receiptBytes = base64Decode(receipt); // 模拟解析出产品ID、交易ID、交易时间 final parsedProductId = "解析出的产品ID"; final parsedTransactionId = "解析出的交易ID"; final parsedTransactionDate = DateTime.fromMillisecondsSinceEpoch(1699999999000); // 验证核心字段 if (purchase.productID != parsedProductId) throw Exception("产品ID不匹配"); if (purchase.purchaseID != parsedTransactionId) throw Exception("交易ID不匹配"); if (purchase.transactionDate != null && purchase.transactionDate!.isAfter(DateTime.now())) throw Exception("交易时间异常"); // 验证通过,发放权益 grantUserBenefit(purchase.productID); } catch (e) { print("iOS本地验证失败: $e"); // 处理失败逻辑,比如提示用户重新购买 } }
Android 端验证
Android的verificationData.serverVerificationData是购买令牌(purchaseToken),localVerificationData是签名字符串。同样推荐服务器端验证,本地验证安全性较低。
服务器端验证(生产环境必用)
- 收集三个核心参数:
purchaseDetails.purchaseID(orderId)purchaseDetails.productID(sku)purchaseDetails.verificationData.serverVerificationData(purchaseToken)
- 把这三个参数发送到你的后端
- 后端调用Google Play Developer API:
- 一次性购买:
purchases.products.get接口 - 订阅:
purchases.subscriptions.get接口
- 一次性购买:
- 后端验证返回结果:
- 订单状态为
PURCHASED orderId和purchaseDetails.purchaseID一致productId和purchaseDetails.productID一致- 交易时间(
purchaseTimeMillis)和purchaseDetails.transactionDate匹配
- 订单状态为
- 验证通过后通知Flutter端发放权益
本地验证(仅调试用)
import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:pointycastle/asymmetric/api.dart'; void verifyAndroidPurchase(PurchaseDetails purchase) { if (purchase.status != PurchaseStatus.purchased && purchase.status != PurchaseStatus.restored) return; final purchaseData = purchase.verificationData.serverVerificationData; final signature = purchase.verificationData.localVerificationData; final publicKey = "你的Google Play公钥(从控制台获取)"; try { // 验证签名 final rsaPublicKey = parsePublicKeyFromPem(publicKey); final signer = RSASigner(SHA256Digest(), "SHA-256/RSA"); signer.init(false, PublicKeyParameter<RSAPublicKey>(rsaPublicKey)); final signatureBytes = base64Decode(signature); final dataBytes = utf8.encode(purchaseData); final isValidSignature = signer.verifySignature(dataBytes, RSASignature(signatureBytes)); if (!isValidSignature) throw Exception("签名验证失败"); // 解析购买数据 final purchaseMap = jsonDecode(purchaseData); if (purchaseMap["productId"] != purchase.productID) throw Exception("产品ID不匹配"); if (purchaseMap["orderId"] != purchase.purchaseID) throw Exception("订单ID不匹配"); final transactionTimestamp = purchaseMap["purchaseTime"] as int; final parsedDate = DateTime.fromMillisecondsSinceEpoch(transactionTimestamp); if (purchase.transactionDate != null && purchase.transactionDate!.difference(parsedDate).inMinutes > 5) throw Exception("交易时间不匹配"); // 验证通过,发放权益 grantUserBenefit(purchase.productID); } catch (e) { print("Android本地验证失败: $e"); } } // 辅助函数:解析PEM格式公钥 RSAPublicKey parsePublicKeyFromPem(String pem) { // 实现公钥解析逻辑,可借助pointycastle库完成 // 此处省略具体实现,需处理PEM头部、尾部,解析ASN.1格式 }
跨平台通用补充
- 防重复发放:用
purchaseDetails.purchaseID作为唯一键,在你的用户数据库中记录已验证的交易ID,避免用户重复触发回调时重复发放权益 - 恢复购买处理:
PurchaseStatus.restored状态的购买,验证逻辑和purchased完全一致,同样需要验证后恢复用户权益 - 异常处理:验证失败时,需记录日志并给用户明确提示(比如"购买验证失败,请联系客服")
内容的提问来源于stack exchange,提问作者Benjamin Az
相关产品推荐
相关产品推荐

