基于Node.js的桌面RPG角色表XP/资源消耗服务端验证咨询
服务端校验核心实现方案
前端校验仅用于优化用户体验,所有可信校验必须在服务端完成,不要信任前端传递的任何敏感数值。
敏感数据全量存储在服务端,不要同步完整规则到前端
用户的XP余额、所有能力的阶梯定价表全量存在服务端的数据库或官方配置文件中,前端仅展示当前用户的XP余额、单个能力当前等级对应的下一级消耗,永远不要把完整定价表、XP计算逻辑下发到前端。用户发起兑换请求时,前端仅需要传递能力ID、目标兑换等级即可,不需要传递消耗XP数值、当前余额等敏感参数。兑换请求全流程校验逻辑
- 先完成身份校验:通过session/Token确认请求对应的用户ID,避免跨用户伪造操作
- 从服务端数据库拉取对应用户的当前XP余额、该能力已持有等级,完全不采信前端传递的相关数值
- 拉取服务端存储的官方阶梯定价表,根据用户当前持有等级、目标等级,自行计算本次兑换需要消耗的总XP,完全不采信前端传递的消耗数值
- 比对用户当前XP >= 总消耗:如果余额不足直接返回错误,拒绝操作
- 校验通过后执行原子操作:先扣除对应XP,再更新用户该能力的持有等级,最后才返回操作成功的结果
- 操作完成后主动将最新的XP余额、能力等级同步给前端做展示更新即可
- 额外防篡改优化
- 全量留操作日志:每个用户的XP获取记录、消耗记录、能力变更记录都永久存在服务端,可随时回溯对账,可定期校验「用户当前XP = 累计获得XP - 累计消耗XP」的恒等式,避免数值异常
- 所有涉及XP计算、定价计算的逻辑统一封装为服务端公共函数,所有相关接口统一调用,避免不同逻辑分支规则不一致出现漏洞
- 涉及数据变更的操作优先用数据库原子操作实现,避免并发请求导致的数值错乱
Node.js 简易实现示例(Express框架)
// 仅存储在服务端的官方定价配置,永远不下发给前端 const abilityPriceConfig = { // 能力ID对应阶梯价格数组,索引为等级,值为升到该级需要的XP "strength": [0, 10, 20, 40, 80], "magic": [0, 15, 30, 60, 120] } // 兑换能力接口 app.post('/api/ability/exchange', async (req, res) => { const { abilityId, targetLevel } = req.body const userId = req.auth.userId // 从鉴权中间件获取当前登录用户ID // 校验参数合法性 if (!abilityPriceConfig[abilityId] || targetLevel < 1 || targetLevel > abilityPriceConfig[abilityId].length - 1) { return res.status(400).json({ msg: "无效的能力或目标等级" }) } // 从服务端拉取用户当前数据 const user = await UserModel.findById(userId) const currentLevel = user.abilities[abilityId] || 0 if (targetLevel <= currentLevel) { return res.status(400).json({ msg: "目标等级不能低于当前已持有等级" }) } // 服务端自行计算总消耗 let totalCost = 0 for (let i = currentLevel + 1; i <= targetLevel; i++) { totalCost += abilityPriceConfig[abilityId][i] } // 校验余额 if (user.xp < totalCost) { return res.status(400).json({ msg: "XP余额不足" }) } // 原子更新用户数据 await UserModel.updateOne( { _id: userId }, { $inc: { xp: -totalCost }, $set: { [`abilities.${abilityId}`]: targetLevel } } ) // 返回最新数据给前端更新展示 const updatedUser = await UserModel.findById(userId, { xp: 1, abilities: 1 }) res.json({ success: true, data: updatedUser }) })
内容的提问来源于stack exchange,提问作者Bob McCroy
相关产品推荐
相关产品推荐

