You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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为空。推荐优先用服务器端验证,本地验证仅用于快速调试。

服务器端验证(生产环境必用)

  1. 把purchaseDetails.verificationData.localVerificationData(收据字符串)发送到你的后端
  2. 后端向Apple验证接口发送POST请求:
    • 沙盒环境:https://sandbox.itunes.apple.com/verifyReceipt
    • 生产环境:https://buy.itunes.apple.com/verifyReceipt
  3. 后端解析Apple返回的响应,重点验证:
    • 响应中的status字段为0(验证通过)
    • receipt.in_app中的product_id和purchaseDetails.productID一致
    • receipt.in_app中的transaction_id和purchaseDetails.purchaseID一致
    • 交易时间(purchase_date_ms)和purchaseDetails.transactionDate匹配,且不是未来时间
  4. 后端返回验证结果给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是签名字符串。同样推荐服务器端验证,本地验证安全性较低。

服务器端验证(生产环境必用)

  1. 收集三个核心参数:
    • purchaseDetails.purchaseID(orderId)
    • purchaseDetails.productID(sku)
    • purchaseDetails.verificationData.serverVerificationData(purchaseToken)
  2. 把这三个参数发送到你的后端
  3. 后端调用Google Play Developer API:
    • 一次性购买:purchases.products.get接口
    • 订阅:purchases.subscriptions.get接口
  4. 后端验证返回结果:
    • 订单状态为PURCHASED
    • orderId和purchaseDetails.purchaseID一致
    • productId和purchaseDetails.productID一致
    • 交易时间(purchaseTimeMillis)和purchaseDetails.transactionDate匹配
  5. 验证通过后通知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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 04:03:04