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

基于Firebase Cloud Functions实现Flutter应用内订阅的安全方案咨询

构建Flutter+Firebase全服务端应用内订阅系统方案

核心思路

全程通过服务端验证订阅状态、同步用户权限,客户端仅负责发起购买请求和读取Firestore中的最终权限标记,完全规避客户端验证带来的安全风险。推荐两种成熟实现路径:


方案一:基于第三方计费服务(RevenueCat/Adapty)快速实现

这类服务已封装跨平台订阅逻辑、收据验证、状态同步等细节,只需通过Webhook触发Cloud Functions更新Firestore,开发效率最高。

步骤1:Flutter客户端集成第三方SDK

以RevenueCat为例,仅处理购买发起和用户ID绑定,不涉及任何状态验证:

import 'package:purchases_flutter/purchases_flutter.dart';

// 初始化SDK(密钥从第三方控制台获取)
Future<void> initSubscriptionService() async {
  await Purchases.configure(PurchasesConfiguration("REVENUECAT_PUBLIC_KEY"));
  // 绑定Firestore用户ID,确保第三方服务与Firestore用户标识一致
  await Purchases.logIn(FirebaseAuth.instance.currentUser!.uid);
}

// 发起订阅购买
Future<void> purchasePremiumSubscription(String productId) async {
  try {
    final offerings = await Purchases.getOfferings();
    final targetPackage = offerings.current?.availablePackages
        ?.firstWhere((pkg) => pkg.product.identifier == productId);
    if (targetPackage != null) {
      await Purchases.purchasePackage(targetPackage);
      // 无需客户端处理状态,等待Cloud Functions同步Firestore
    }
  } catch (e) {
    // 处理购买失败(如用户取消、支付错误)
  }
}

// 仅从Firestore读取权限状态
Stream<bool> listenToPremiumStatus() {
  final userId = FirebaseAuth.instance.currentUser!.uid;
  return FirebaseFirestore.instance.collection("users").doc(userId)
      .snapshots()
      .map((doc) => doc.data()?["isPremium"] ?? false);
}

步骤2:配置第三方服务Webhook

在RevenueCat/Adapty控制台中,添加Webhook端点为你的Cloud Functions HTTPS URL,并设置签名密钥(用于后续请求合法性验证)。

步骤3:编写Cloud Functions处理Webhook事件

以Node.js为例,验证请求合法性后更新Firestore用户状态:

const functions = require("firebase-functions");
const admin = require("firebase-admin");
const crypto = require("crypto");

admin.initializeApp();
// 从Firebase环境变量中读取第三方服务的Webhook密钥
const WEBHOOK_SECRET = functions.config().revenuecat.webhook_secret;

exports.handleSubscriptionWebhook = functions.https.onRequest(async (req, res) => {
  // 验证Webhook签名,防止恶意请求
  const signature = req.headers["x-revenuecat-signature"];
  const payload = req.rawBody.toString();
  const hmac = crypto.createHmac("sha256", WEBHOOK_SECRET);
  const digest = Buffer.from(hmac.update(payload).digest("hex"), "utf8");
  const signatureBuffer = Buffer.from(signature, "utf8");

  if (!crypto.timingSafeEqual(digest, signatureBuffer)) {
    res.status(403).send("Invalid request signature");
    return;
  }

  const event = req.body;
  const userId = event.app_user_id;
  const eventType = event.event_type;

  let isPremium = false;
  let expiryDate = null;

  switch (eventType) {
    case "SUBSCRIPTION_ACTIVATED":
    case "SUBSCRIPTION_RENEWED":
    case "SUBSCRIPTION_UNCANCELLED":
      isPremium = true;
      expiryDate = event.expires_at_ms ? new Date(event.expires_at_ms) : null;
      break;
    case "SUBSCRIPTION_EXPIRED":
    case "SUBSCRIPTION_CANCELLED":
      isPremium = false;
      break;
    default:
      // 忽略无关事件(如试用开始等)
      res.status(200).send("Event ignored");
      return;
  }

  // 更新Firestore用户文档
  await admin.firestore().collection("users").doc(userId).update({
    isPremium: isPremium,
    subscriptionExpiry: expiryDate
  });

  res.status(200).send("User status updated");
});

方案二:直接对接Google Play/Apple Store原生通知

无需第三方服务,直接通过平台官方Webhook实现状态同步,适合需要高度定制化的场景。

步骤1:Flutter客户端传递购买凭证到Cloud Functions

使用in_app_purchase包发起购买,购买成功后将凭证发送到Cloud Functions:

import 'package:in_app_purchase/in_app_purchase.dart';

Future<void> purchaseAndSendReceipt(String productId) async {
  final purchaseParam = PurchaseParam(productDetails: await getProductDetails(productId));
  final purchaseResult = await InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam);
  
  if (purchaseResult.pendingPurchase.isNotEmpty) {
    final purchase = purchaseResult.pendingPurchase.first;
    // 将购买凭证发送到Cloud Functions
    await FirebaseFunctions.instance.httpsCallable("validatePurchase")
        .call({
          "userId": FirebaseAuth.instance.currentUser!.uid,
          "receiptData": purchase.verificationData.serverVerificationData,
          "platform": defaultTargetPlatform.name
        });
  }
}

步骤2:Cloud Functions验证凭证并初始化状态

编写函数调用平台官方API验证凭证,确认订阅有效后更新Firestore:

exports.validatePurchase = functions.https.onCall(async (data, context) => {
  const { userId, receiptData, platform } = data;
  
  // 验证用户身份(确保请求来自合法客户端用户)
  if (!context.auth || context.auth.uid !== userId) {
    throw new functions.https.HttpsError("permission-denied", "Unauthorized");
  }

  let isPremium = false;
  let expiryDate = null;

  if (platform === "Android") {
    // 调用Google Play Developer API验证purchaseToken
    const playResponse = await verifyGooglePlayReceipt(receiptData);
    isPremium = playResponse.purchaseState === 0; // 0代表有效订阅
    expiryDate = playResponse.expiryTimeMillis ? new Date(playResponse.expiryTimeMillis) : null;
  } else if (platform === "iOS") {
    // 调用Apple App Store Server API验证receipt
    const appleResponse = await verifyAppleReceipt(receiptData);
    isPremium = appleResponse.status === 0 && appleResponse.latestReceiptInfo?.length > 0;
    expiryDate = appleResponse.latestReceiptInfo?.[0].expiresDateMs ? new Date(appleResponse.latestReceiptInfo[0].expiresDateMs) : null;
  }

  await admin.firestore().collection("users").doc(userId).update({
    isPremium: isPremium,
    subscriptionExpiry: expiryDate
  });

  return { success: true };
});

步骤3:配置平台实时通知

  • Google Play:开启实时开发者通知(RTDN),指向Cloud Functions端点,处理订阅到期、续订、取消等事件。
  • Apple Store:配置App Store服务器通知,将通知发送到Cloud Functions,验证JWT签名后更新Firestore用户状态。

关键安全注意事项

  1. 所有服务端请求必须验证签名(第三方Webhook签名、平台通知签名),防止伪造请求。
  2. Cloud Functions中必须验证用户身份(通过context.auth),确保仅合法用户能触发状态更新。
  3. 客户端永远不处理订阅状态逻辑,仅读取Firestore的最终标记。

内容的提问来源于stack exchange,提问作者Alex Stroescu

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 18:50:28