本文档描述塔防游戏排行榜的安全实现策略。
游戏逻辑在客户端执行以保证流畅体验,但客户端环境完全不可信:
- 客户端代码可被逆向、修改
- 网络请求可被拦截、篡改
- 系统时间可被修改
- 内存数据可被读取、修改
数据伪造:
- 伪造击杀数量(声称击杀 100 只,实际 10 只)
- 伪造金钱收益(声称获得 1000 金币,实际 100 金币)
- 伪造得分(直接修改 scoreGained)
- 伪造伤害数据(声称造成 10000 伤害,实际 100 伤害)
逻辑绕过:
- 跳过建筑成本扣除(免费建造)
- 修改建筑属性(无限射程、瞬间攻击)
- 修改怪物属性(1 血怪物、静止不动)
重放攻击:
- 重复提交同一波次数据
- 使用他人的有效数据包
时间操控:
- 修改系统时间伪造游戏时长
- 暂停游戏期间修改状态
采用分层验证策略,每层有不同的检测目标和计算成本。
目标:拦截明显的数据不一致。
验证内容:
- 击杀数量一致性:
sum(killedByType) == killed - 击杀上限:
killedByType[type] <= wave_config[type].count - 数量守恒:
killed + passed + remaining == spawned - 金钱收益:
moneyGained == sum(killedByType[type] * monster[type].money) - 生成数量:
spawned <= total_monsters
特点:计算简单、响应快、覆盖明显作弊。
目标:检测伤害数据的合理性。
验证内容:
- 伤害下限:
totalDamageDealt >= totalLifeDestroyed - DPS 容量验证:
totalDamageDealt <= max_dps * waveDurationFrames * 1.1
DPS 容量计算:
max_dps = sum(building.damage / building.attack_interval for building in validation_buildings)
注意:使用的是 validation_buildings(波次期间存在过的建筑),而非波次结束时的 submitted_buildings。因为攻击可能发生在建筑被出售之前,验证需要考虑所有参与过攻击的建筑。
10% 容差用于处理浮点精度和边界情况。
攻击事件验证:
- 伤害总和:
sum(attacks.damage) == totalDamageDealt - 帧号时序:攻击帧号必须递增
- 怪物 ID 验证:必须是服务端下发的 UUID
- 射程验证:
originalTargetPosition在建筑射程内 - 伤害值范围:
1 <= damage <= building_damage_at_level - 累计伤害验证:被击杀怪物的累计伤害 >= 生命值
- 路径方向验证:怪物整体移动方向应朝向出口(仅记录日志,不阻断)
射程验证说明:
验证的是发射时目标位置(originalTargetPosition),而非命中时位置。这允许合法的"误伤"场景(子弹命中路径上的其他怪物)。
路径验证说明:
路径验证检查怪物是否整体朝出口方向移动。由于以下原因,此验证仅记录日志而不阻断请求:
- 怪物有 10% 概率随机重新寻路
- 玩家可以移除路障导致怪物掉头走新路径
- 这些都是合法的游戏行为,但会导致怪物暂时远离出口
- 应对:依赖 DPS 容量验证(绕路会增加怪物存活时间,受 DPS 上限约束)
目标:消除怪物寻路的随机性,使服务端能精确重建怪物路径。
TODO 计划:
- 种子随机数生成器
- 服务端在
firstWave/nextWave中下发随机种子 - 前端使用种子初始化 PRNG(如 xorshift128+)
- 10% 重新寻路判定改用 PRNG 而非
Math.random() - 服务端可用相同种子重现随机序列
- 服务端在
- 确定性寻路验证
- 服务端根据种子重建怪物的寻路决策序列
- 验证客户端提供的怪物位置是否符合预期
- 检测位置伪造和路径操控
预期收益:
- 服务端可独立计算怪物在任意帧的位置
- 射程验证不再依赖客户端提供的位置
- 可检测怪物位置伪造
目标:检测历史数据异常。
检测项:
- 击杀率突变:历史 < 50%,当前 > 95%
- 效率突变:当前波和历史都有花费时,当前效率 > 历史效率 * 3
注意:效率检测仅在当前波次和历史波次都有建筑花费时触发。跳过检测的情况:
- 当前无花费:玩家在前期集中投资后不再建造是正常策略
- 历史无花费:无法计算有意义的历史效率基准
特点:仅记录日志,不阻断请求。用于事后分析和人工审核。
得分基于攻击事件计算,而非客户端声称的值:
expected_score = sum(floor(sqrt(attack.damage)) for attack in attacks)
服务端独立计算得分,与客户端提交值比对。
提交 /sessions/end
+-- 验证 sessionId 有效性
| - 会话不存在或过期时返回错误
+-- 如有 lastWave,执行波次验证
| - Level 1 -> Level 2
| - 任一失败则拒绝入榜
+-- 检查最终得分
| - score == 0: 拒绝入榜(至少需击杀一只怪物)
+-- 执行 Level 4 统计分析
| - 记录异常日志,不阻断
+-- 创建 LeaderboardEntry
+-- 返回排名信息
最终得分从 WaveRecord 累加,而非客户端声称:
final_score = sum(wave_record.score_gained for wave_record in session.wave_records)每波的 score_gained 在波次验证通过后由服务端存储,不依赖客户端累计值。
0 分不能上榜:
- 防止刷空记录占位
- 确保排行榜有实际游戏内容
- 最低要求:完成至少一次有效攻击
会话唯一性:
- 每次 POST /sessions 创建新的 UUID
- sessionId 不可预测,无法猜测他人会话
会话生命周期:
- 创建:POST /sessions
- 更新:POST /sessions/wave(波次提交)
- 结束:POST /sessions/end(成功后删除 session)或 24 小时过期清理
防重复提交:
提交成功后立即删除 session,确保同一 sessionId 只能成功提交一次:
with transaction.atomic():
entry = LeaderboardEntry.objects.create(...)
session.delete()- 第一次提交:查询 session 成功,创建排行榜记录,删除 session
- 第二次提交:查询 session 失败(DoesNotExist),返回 404
- 事务保证:创建记录和删除 session 原子执行,不会出现中间状态
会话状态追踪:
class GameSession:
wave_count: int # 已完成波次数
next_wave: JSON # 下一波配置(含怪物 UUID)波次号验证:
- 必须连续提交:wave 1 -> wave 2 -> wave 3
- 不能跳波:不能从 wave 1 直接提交 wave 3
- 不能重复:wave 2 提交后不能再次提交 wave 2
实现方式:
if request.wave_number != session.wave_count + 1:
return error("波次不连续")UUID 生成:
- 每波怪物由服务端生成唯一 UUID
- UUID 在 nextWave 中下发给客户端
- 客户端必须使用这些 UUID 记录攻击
验证作用:
- 防止客户端凭空创造怪物
- 确保攻击记录对应真实的服务端怪物
- 支持累计伤害验证(追踪单个怪物受到的总伤害)
验证逻辑:
for attack in attacks:
if attack.monsterId not in wave_monster_ids:
return error("怪物 ID 不是服务端下发的 UUID")设计理由:
- 系统时间可被篡改
- 帧号只在游戏运行时递增
- 暂停时帧号不增长
验证作用:
- DPS 容量验证基于帧号计算实际游戏时长
- 攻击间隔验证基于帧号差
- 防止通过修改时间绕过验证
波次 11+ 使用随机生成算法:
- 组大小:随机 1-3
- 怪物类型:随机 0-8
验证作用:
- 服务端生成完整怪物配置(含唯一 ID)并下发给客户端
- 验证时通过怪物 ID 精确匹配
- 防止客户端伪造怪物或篡改属性
Level 3 未实现:
确定性寻路验证未实现。
- 原因:需要移除或改造 10% 随机重新寻路机制
- 影响:射程验证依赖客户端提供的怪物位置
- 应对:Level 2 的攻击事件验证提供部分覆盖
路径验证仅记录日志:
路径验证检查怪物是否整体朝出口方向移动,但不阻断请求,仅记录日志。
- 原因:怪物有 10% 概率随机重新寻路,且玩家可以移除路障导致怪物掉头
- 影响:无法检测怪物绕远路的操控
- 应对:依赖 DPS 容量验证(绕路会增加存活时间,受 DPS 上限约束)
统计分析不阻断:
Level 4 只记录日志,不拒绝请求。
- 原因:避免误杀正常玩家的突破性表现
- 影响:需要人工审核异常记录
- 应对:建立审核机制,必要时手动删除可疑成绩
子弹碰撞检测:
客户端负责碰撞检测,服务端无法验证子弹是否真的命中。
- 风险:客户端可伪造命中(子弹未到达即判定命中)
- 缓解:DPS 容量验证限制了总伤害上限
怪物位置可信度:
攻击记录中的怪物位置由客户端提供。
- 风险:客户端可伪造位置使射程验证通过
- 缓解:路径方向验证提供部分约束
精确时序:
攻击间隔验证依赖客户端帧号。
- 风险:客户端可调整帧号间隔
- 缓解:DPS 容量验证作为总量约束
安全性 vs 体验:
选择客户端执行以保证流畅体验,接受无法完全防止作弊。
开发成本 vs 防护深度:
跳过 Level 3 确定性寻路验证,用 Level 1 + 2 + 4 覆盖大部分场景。
防护 vs 误杀:
Level 4 不阻断请求,避免误伤正常玩家。
- 实现 Level 3 确定性寻路验证
- 引入机器学习异常检测
- 客户端完整性校验(代码签名)
- 建立人工审核工作流