Firebase如何实现服务端校验后修改数据库,不开放用户直接写权限
Firebase 许可证激活服务端校验方案
你要实现的逻辑核心是把所有敏感操作收拢到Firebase可信服务端环境执行,全程不给客户端开放敏感数据的写权限,具体落地步骤如下:
核心流程闭环
- 客户端仅负责收集用户输入的许可证密钥,携带当前登录用户身份发起请求,不执行任何敏感写操作
- 所有许可证有效性校验、数据更新逻辑全部在服务端可信环境完成
- 数据库安全规则从根源锁死客户端对许可证表、用户订阅字段的写权限,从规则层避免越权
第一步:配置最小权限的数据库安全规则
先从规则层堵死越权可能,以Firestore为例,安全规则配置如下:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // 许可证存储集合:所有客户端完全禁止读写,仅服务端管理员账号可访问 match /licenses/{licenseId} { allow read, write: if false; } // 用户信息集合:用户仅能读取自身文档,无订阅相关字段的写权限 match /users/{userId} { allow read: if request.auth != null && request.auth.uid == userId; // 仅允许用户更新昵称、头像这类非敏感字段,订阅相关字段完全禁写 allow write: if request.auth != null && request.auth.uid == userId && !request.resource.data.keys().hasAny(["subscriptionTier", "subscriptionExpireAt", "activatedLicense"]); } } }
你需要提前在licenses集合中录入所有合法许可证,每个许可证文档包含字段:密钥哈希值、是否有效、绑定用户ID(未激活为空)、到期时间,这个集合客户端完全无法访问,不会出现密钥泄露、越权篡改的问题。
第二步:部署服务端可信接口
最省事的方案是使用Firebase Callable Cloud Functions,不需要自己搭服务器、配鉴权逻辑,平台自动校验请求方的登录身份,最小可用的实现代码如下(Node.js环境):
const functions = require('firebase-functions/v1'); const admin = require('firebase-admin'); const crypto = require('crypto'); admin.initializeApp(); const db = admin.firestore(); // 许可证激活接口 exports.activateLicense = functions.https.onCall(async (data, context) => { // 校验登录态,未登录直接拒绝 if (!context.auth) { throw new functions.https.HttpsError('unauthenticated', '请先登录后再操作'); } const userId = context.auth.uid; const inputKey = data.licenseKey?.trim(); if (!inputKey) { throw new functions.https.HttpsError('invalid-argument', '请输入有效的许可证密钥'); } // 服务端查询许可证信息做校验,全程逻辑不暴露给客户端 const keyHash = crypto.createHash('sha256').update(inputKey).digest('hex'); const licenseSnapshot = await db.collection('licenses') .where('keyHash', '==', keyHash) .limit(1) .get(); if (licenseSnapshot.empty) { throw new functions.https.HttpsError('not-found', '许可证密钥无效'); } const licenseDoc = licenseSnapshot.docs[0]; const licenseData = licenseDoc.data(); // 多维度校验许可证状态 if (!licenseData.isValid) { throw new functions.https.HttpsError('failed-precondition', '许可证已被禁用'); } if (licenseData.activatedUid && licenseData.activatedUid !== userId) { throw new functions.https.HttpsError('already-exists', '该许可证已绑定其他账号'); } if (licenseData.expireAt && licenseData.expireAt.toDate() < new Date()) { throw new functions.https.HttpsError('deadline-exceeded', '许可证已过期'); } // 校验通过,用服务端管理员权限原子更新两份数据,避免状态不一致 const batch = db.batch(); // 更新许可证绑定信息 batch.update(licenseDoc.ref, { activatedUid: userId, activatedAt: admin.firestore.FieldValue.serverTimestamp() }); // 更新用户订阅状态为高级版 batch.update(db.collection('users').doc(userId), { subscriptionTier: 'premium', activatedLicenseKeyHash: keyHash, subscriptionExpireAt: licenseData.expireAt, updatedAt: admin.firestore.FieldValue.serverTimestamp() }); await batch.commit(); // 返回成功结果给客户端 return { success: true, subscriptionTier: 'premium', expireAt: licenseData.expireAt } });
如果你习惯自己维护后端服务,只需要在自己的服务端集成Firebase Admin SDK,逻辑和上面完全一致即可,注意绝对不要把Admin SDK的服务账号密钥泄露到客户端。
第三步:客户端对接逻辑
客户端不需要任何数据库写操作,逻辑非常简单:
- 用户提交许可证时,直接调用上面部署的Callable云函数,传入用户输入的密钥
- 收到接口返回的成功响应后,重新拉取一次当前用户的信息文档,确认订阅状态更新后,给用户展示激活成功的提示和高级权益
- 如果接口返回错误,直接把对应错误提示展示给用户即可
可选加固项
- 给激活接口加频率限制:单个用户1分钟内最多调用3次,防止暴力撞库猜密钥
- 配置定时任务:每天自动扫描到期的高级订阅,把对应用户的订阅状态改回标准档
- 加操作日志:所有激活、订阅变更操作都存在单独的日志集合,方便后续排查问题
内容的提问来源于stack exchange,提问作者timnik
相关产品推荐
相关产品推荐

