基于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用户状态。
关键安全注意事项
- 所有服务端请求必须验证签名(第三方Webhook签名、平台通知签名),防止伪造请求。
- Cloud Functions中必须验证用户身份(通过
context.auth),确保仅合法用户能触发状态更新。 - 客户端永远不处理订阅状态逻辑,仅读取Firestore的最终标记。
内容的提问来源于stack exchange,提问作者Alex Stroescu
相关产品推荐
相关产品推荐

