如何防止用户篡改APK以修改Firestore中用户的Credit余额?
防止客户端篡改Firestore金币余额的解决方案
核心原则:客户端完全不可信,所有涉及余额变更的操作必须在服务端或通过安全规则做严格校验,绝不能让客户端直接控制余额数值。
1. 用Firestore安全规则限制非法写入
不要给客户端直接修改余额字段的权限,只允许符合业务逻辑的增量/减量操作,同时在规则里做校验:
- 仅允许登录用户修改自己的
Credit文档 - 限制余额只能做合法变更:比如扣除金币时,新余额必须小于等于原余额,且扣除金额为正数;增加金币时(比如奖励),可结合业务字段验证操作合法性(如是否有对应的任务完成记录)
- 禁止客户端替换整个文档,只能更新指定字段
示例安全规则:
match /users/{userId}/Credit/{creditDocId} { // 仅允许登录用户操作自己的余额文档 allow read: if request.auth != null && request.auth.uid == userId; allow write: if request.auth != null && request.auth.uid == userId // 仅允许两种操作:扣减或增加余额,且操作参数合法 && ( // 扣减逻辑:新余额 = 原余额 - 扣减金额,且扣减金额>0,新余额≥0 (request.data.deduct is number && request.data.deduct > 0 && request.resource.data.balance == resource.data.balance - request.data.deduct && request.resource.data.balance >= 0) || // 增加逻辑:新余额 = 原余额 + 增加金额,且增加金额>0 (request.data.add is number && request.data.add > 0 && request.resource.data.balance == resource.data.balance + request.data.add) ); }
客户端调用时,只能通过指定的deduct或add参数发起操作,比如用FieldValue.increment()配合规则校验,而不是直接设置balance的值。
2. 将余额变更逻辑完全移到服务端(Cloud Functions)
这是最安全的方案:不让客户端直接触碰Credit文档,所有余额变更请求都通过Cloud Functions处理,所有校验逻辑在服务端完成:
- 客户端调用云函数,传入操作类型(如购买商品、领取任务奖励)和必要参数(如商品ID、任务ID)
- 云函数先验证用户身份,再校验操作的合法性(比如从服务端数据库读取商品真实价格,检查用户是否完成任务)
- 最后通过事务修改Firestore余额,避免并发问题
示例Node.js云函数:
const functions = require("firebase-functions"); const admin = require("firebase-admin"); admin.initializeApp(); exports.processCreditChange = functions.https.onCall(async (data, context) => { // 验证用户是否登录 if (!context.auth) { throw new functions.https.HttpsError("unauthenticated", "请先登录"); } const userId = context.auth.uid; const { operationType, targetId } = data; // 根据操作类型做不同校验 let changeAmount = 0; if (operationType === "BUY_PRODUCT") { // 从服务端读取商品真实价格,拒绝信任客户端传的价格 const productDoc = await admin.firestore().collection("products").doc(targetId).get(); if (!productDoc.exists) { throw new functions.https.HttpsError("not-found", "商品不存在"); } changeAmount = -productDoc.data().price; // 扣减为负数 if (changeAmount >= 0) { throw new functions.https.HttpsError("invalid-argument", "商品价格非法"); } } else if (operationType === "CLAIM_REWARD") { // 验证用户是否完成该任务 const taskRecordDoc = await admin.firestore().collection("userTasks").doc(`${userId}_${targetId}`).get(); if (!taskRecordDoc.exists || !taskRecordDoc.data().completed) { throw new functions.https.HttpsError("failed-precondition", "未完成任务,无法领取奖励"); } // 读取奖励金额 const taskDoc = await admin.firestore().collection("tasks").doc(targetId).get(); changeAmount = taskDoc.data().reward; if (changeAmount <= 0) { throw new functions.https.HttpsError("invalid-argument", "奖励金额非法"); } // 标记奖励已领取,避免重复领取 await taskRecordDoc.ref.update({ rewardClaimed: true }); } else { throw new functions.https.HttpsError("invalid-argument", "不支持的操作类型"); } // 用事务修改余额,防止并发冲突 const creditDocRef = admin.firestore().collection("users").doc(userId).collection("Credit").doc("userCredit"); const newBalance = await admin.firestore().runTransaction(async (t) => { const creditDoc = await t.get(creditDocRef); if (!creditDoc.exists) { // 首次使用,初始化余额为0 t.set(creditDocRef, { balance: 0 + changeAmount }); return changeAmount; } const currentBalance = creditDoc.data().balance; if (currentBalance + changeAmount < 0) { throw new functions.https.HttpsError("failed-precondition", "余额不足"); } t.update(creditDocRef, { balance: currentBalance + changeAmount }); return currentBalance + changeAmount; }); return { newBalance }; });
3. 额外的安全加固措施
- 不在客户端存储敏感数据:不要在APK里硬编码商品价格、奖励金额等数值,所有业务数据都从服务端实时获取
- 启用审计日志:开启Firestore的审计功能,监控所有余额变更操作,发现异常(比如频繁无理由增加余额)及时处理
- 请求签名校验:客户端发起请求时,带上服务端生成的签名,服务端验证签名合法性后再处理,防止篡改请求参数
- 定期一致性校验:定时运行云函数,对比用户余额和业务记录(如购买记录、奖励记录),确保余额和操作流水一致
内容的提问来源于stack exchange,提问作者faccio
相关产品推荐
相关产品推荐

