Appearance
反作弊与安全
客户端为什么不可信?
核心解释
客户端不可信,是因为客户端运行在玩家自己的机器上。玩家理论上可以改内存、改代码、Hook 函数、抓包改包、伪造请求、重放旧消息。所以服务器不能直接相信客户端上报的“结果”。
为什么不可信
客户端代码可以被反编译、修改、注入。
客户端内存里的金币、血量、CD、位置都可能被改。
网络包也可能被抓包、篡改、重放。
本地时间和帧率也可能被改,形成加速、无 CD、瞬移之类的问题。
所以客户端说:
我打中了
我造成了 999999 伤害
我获得了 100000 金币
我已经通关了
我当前位置是终点服务器都不能直接相信。
服务器应该信什么
服务器可以接收客户端的“意图”,但不能直接相信客户端的“结果”。
比如客户端可以说:
我按了移动键
我想释放技能 3
我点击了这个目标
我请求领取奖励但服务器必须自己判断:
你是否真的有这个技能
技能 CD 是否好了
蓝量是否足够
距离是否够
目标是否还活着
是否在可攻击范围内
是否已经领取过奖励
这次请求是否重复最终结果应该由服务器计算。
举个战斗例子
不安全做法:
客户端:我打中了怪物,造成 5000 伤害
服务器:好的,怪物扣血这样很容易被改包。
安全做法:
客户端:我在 Tick 120 使用了技能 3,目标是怪物 5
服务器:检查技能 CD、距离、朝向、碰撞、目标状态
服务器:计算是否命中和造成多少伤害
服务器:广播最终结果这里客户端只是提交操作,服务器才负责裁决。
项目里怎么设计
客户端负责:
输入采集
本地预测
动画表现
特效播放
UI 展示
弱提示服务器负责:
角色真实属性
技能 CD
伤害计算
掉落结算
背包变化
任务进度
排行榜分数
战斗胜负也就是说,客户端可以“先演”,但服务器必须“算账”。
常见防护手段
服务器权威
消息序号
ACK 和去重
请求限频
协议签名
时间戳校验
反重放
状态合法性校验
关键日志审计
异常行为检测但要记住:加密、签名、防外挂都只是提高作弊成本,不应该替代服务器权威校验。
面试高分回答
NOTE
客户端不可信的根本原因是它运行在玩家可控环境里,代码、内存、输入和网络包都有可能被修改。服务器不能相信客户端上报的结果,比如伤害、金币、掉落、命中和结算,只能把客户端消息当成操作意图。正确做法是客户端发送输入或请求,服务器根据权威状态校验 CD、距离、资源、碰撞、权限和序号,然后由服务器计算最终结果并同步给客户端。客户端可以做预测和表现,但关键规则和资产变化必须由服务器裁决。
一句话记忆
客户端负责“我想做什么”,服务器负责“你能不能做,以及结果是什么”。
哪些逻辑必须放服务端?
核心原则
凡是影响公平、资产、进度、排名、结算的逻辑,都必须放服务端。客户端可以负责输入和表现,但不能决定最终结果。
必须放服务端的逻辑
账号和权限:
登录鉴权
Token 校验
账号状态
封禁状态
权限判断资产和交易:
金币
钻石
道具
背包
商城购买
充值发货
邮件附件
交易系统
抽卡结果战斗结算:
伤害计算
命中判定
暴击判定
技能 CD
蓝量消耗
Buff 生效
死亡判定
掉落归属
战斗胜负成长和进度:
经验
等级
任务进度
成就
副本结算
每日奖励
活动奖励公共竞争数据:
排行榜
匹配
赛季分
竞技场结算
公会贡献
拍卖行
世界 Boss 归属随机结果:
抽卡
掉落
暴击
随机词条
宝箱奖励这些如果放客户端,玩家改内存、改包、改时间,就可能直接刷资源、刷伤害、刷排名。
客户端可以做什么
客户端可以做:
输入采集
本地预测
动画表现
特效播放
音效播放
UI 展示
弱网提示
临时表现状态比如玩家点击释放技能,客户端可以先播动作、先做特效,让手感更好。
但最终是否命中、造成多少伤害、消耗多少蓝、进入多少 CD,必须由服务器判断。
正确设计方式
客户端不要上报结果,而是上报意图。
不要这样:
客户端:我打中了,造成 5000 伤害
服务器:好的,扣血应该这样:
客户端:我在 Tick 120 使用技能 3,目标是怪物 5
服务器:检查 CD、蓝量、距离、状态、碰撞、目标是否合法
服务器:计算命中和伤害
服务器:广播最终结果这就是“客户端请求,服务器裁决”。
灰色区:移动逻辑
移动比较特殊。
为了手感,客户端通常会做本地预测:
客户端先移动
服务器校验速度和位置
发现异常就纠正但服务器仍然要检查:
是否超速
是否穿墙
是否瞬移
是否无视控制状态
是否在合法区域所以移动可以客户端先表现,但服务端必须有权威校验和纠偏。
面试高分回答
IMPORTANT
我会按影响范围来划分。凡是影响资产、公平、进度、排行榜和最终结算的逻辑,都必须放服务端,比如伤害、命中、技能 CD、掉落、背包、金币、任务、排行榜和交易。客户端只负责输入、预测和表现,不能直接决定结果。客户端发送的是“我想做什么”,比如释放技能或领取奖励;服务端根据权威状态校验条件,计算最终结果,保存数据并同步给客户端。这样可以防止改内存、改包、重放请求导致作弊。
一句话记忆
客户端负责“演出来”,服务器负责“算清楚”。
移速外挂如何检测?
核心原则
移速外挂检测的本质是:客户端说“我到了哪里”不能直接信,服务端要根据自己的时间、角色规则、地图合法性和历史轨迹判断这个移动是否合理。
怎么检测
最基础的检测是距离除以时间:
实际移动距离 = 本次位置 - 上次位置
允许移动距离 = 最大速度 * 时间差 + 容差如果:
实际移动距离 > 允许移动距离就说明玩家可能超速。
但项目里不能只看这一条,因为网络抖动、丢包、冲刺、击退、传送、技能位移都会影响结果。
所以服务端一般要综合判断:
角色当前最大速度
是否有加速 Buff
是否有减速 Debuff
是否正在冲刺
是否被击退
是否眩晕或定身
是否使用了合法传送技能
移动路径是否穿墙
目标点是否在 NavMesh 上
是否跨越不可行走区域服务端权威做法
更安全的方式不是让客户端上报最终位置,而是让客户端上报输入:
我按了方向键
我想往这个方向移动
我在这个 Tick 开始冲刺然后服务端自己模拟移动。
客户端可以本地预测,让画面流畅;服务端负责权威校验。如果客户端预测位置和服务端位置不一致,就以服务端为准,把客户端拉回或平滑纠偏。
C# 简化检测代码
c
using UnityEngine; // 引入 Unity 的 Vector3 类型。
public sealed class ServerMoveValidator // 定义服务端移动校验类。
{ // 类开始。
private Vector3 lastPosition; // 保存服务端记录的上一次权威位置。
private float lastServerTime; // 保存上一次收到移动包时的服务端时间。
private float maxSpeed = 5.0f; // 保存角色当前最大移动速度,真实项目里会由属性系统计算。
private float tolerance = 0.5f; // 保存容差,用来兼容网络抖动和浮点误差。
public bool CheckMove(Vector3 clientPosition, float nowServerTime) // 校验客户端上报的位置是否合法。
{ // 方法开始。
float deltaTime = nowServerTime - lastServerTime; // 用服务端时间计算两次移动之间的时间差。
if (deltaTime <= 0.0f) // 如果时间差不合法。
{ // 判断开始。
return false; // 直接判定为异常移动。
} // 判断结束。
float distance = Vector3.Distance(lastPosition, clientPosition); // 计算客户端本次位置和上次权威位置之间的距离。
float allowedDistance = maxSpeed * deltaTime + tolerance; // 计算这段时间内理论允许移动的最大距离。
if (distance > allowedDistance) // 如果实际移动距离超过允许距离。
{ // 判断开始。
return false; // 认为这次移动可疑,需要拒绝、纠偏或记录嫌疑分。
} // 判断结束。
lastPosition = clientPosition; // 移动合法时,更新服务端记录的权威位置。
lastServerTime = nowServerTime; // 移动合法时,更新服务端记录的时间。
return true; // 返回移动合法。
} // 方法结束。
} // 类结束。处罚不要太粗暴
不要“一次超速就封号”。更稳的做法是:
轻微异常:服务端纠偏
多次异常:累计嫌疑分
持续异常:踢下线
严重异常:封禁或进入人工复核因为真实网络环境里会有延迟抖动,服务端要避免误伤正常玩家。
面试高分回答
TIP
移速外挂一般不能只靠客户端检测,必须服务端校验。服务端记录玩家上一次权威位置和时间,根据角色当前最大速度、Buff、Debuff、冲刺、击退等状态计算允许移动距离,再和客户端上报位置或服务端模拟结果比较。如果移动距离超过 maxSpeed * deltaTime + tolerance,就认为可疑。同时还要做路径合法性检测,比如是否穿墙、是否越过不可行走区域、是否离开 NavMesh。实际项目里通常不会一次异常就封号,而是先纠偏、记录日志和嫌疑分,多次或持续异常再踢下线或封禁。
一句话记忆
移速检测就是:客户端说自己跑多远不算数,服务端用时间、速度、状态和地图规则重新算一遍。
CD 篡改如何检测?
核心原则
CD 篡改检测的关键是:客户端 CD 只能用于 UI 显示,不能作为技能能否释放的依据。真正的 CD 必须由服务端保存、计算和裁决。
为什么客户端 CD 不可信
玩家可以改客户端内存,把技能 CD 改成 0。
也可以改包,伪造:
我的技能已经冷却好了
我要释放技能 3所以服务器不能相信客户端上报的 CD 状态。
客户端可以说:
我想释放技能 3但服务器必须自己判断:
技能 CD 好了吗
蓝量够不够
角色是否沉默
角色是否死亡
目标是否合法
距离是否够
请求是不是重复包服务端怎么检测
服务端给每个玩家维护技能 CD 状态:
skillId
lastCastTime
cooldownDuration收到技能请求时,用服务端时间判断:
serverNow >= lastCastTime + cooldownDuration如果没到时间,直接拒绝。
如果频繁提前请求,就记录异常次数或嫌疑分。
C# 简化代码
c
using System; // 引入基础类型。
using System.Collections.Generic; // 引入 Dictionary 字典。
public sealed class SkillCooldownValidator // 定义技能 CD 校验器。
{ // 类开始。
private readonly Dictionary<int, double> lastCastTimes = new Dictionary<int, double>(); // 保存每个技能上次释放的服务端时间。
private readonly Dictionary<int, double> cooldowns = new Dictionary<int, double>(); // 保存每个技能的 CD 时长配置。
public void SetCooldown(int skillId, double cooldown) // 设置某个技能的 CD 配置。
{ // 方法开始。
cooldowns[skillId] = cooldown; // 把技能 ID 和 CD 时长保存到字典中。
} // 方法结束。
public bool TryCastSkill(int skillId, double serverNow) // 尝试释放技能,并由服务端判断是否允许。
{ // 方法开始。
double cooldown = cooldowns.TryGetValue(skillId, out double value) ? value : 0.0; // 读取技能 CD,如果没有配置就默认 0。
double lastCastTime = lastCastTimes.TryGetValue(skillId, out double last) ? last : double.NegativeInfinity; // 读取上次释放时间,如果没放过就认为无限久以前。
double nextReadyTime = lastCastTime + cooldown; // 计算下一次允许释放的服务端时间。
if (serverNow < nextReadyTime) // 如果当前服务端时间还没到冷却结束时间。
{ // 判断开始。
return false; // 拒绝释放,说明技能还在 CD 中。
} // 判断结束。
lastCastTimes[skillId] = serverNow; // 技能允许释放时,更新上次释放时间。
return true; // 返回释放成功。
} // 方法结束。
} // 类结束。项目里要注意
不要用客户端时间判断 CD。
不要让客户端上报“剩余 CD”。
不要让客户端决定“技能释放成功”。
服务端应该返回权威结果:
释放成功
释放失败
失败原因:CD 未完成
剩余 CD 时间客户端收到后再刷新 UI。
异常处理
不要一次 CD 异常就封号,因为可能有延迟、重发、预测误差。
更稳的是:
第一次提前请求:拒绝
短时间多次提前请求:记录日志
持续大量异常:累计嫌疑分
严重异常:踢下线或封禁面试高分回答
NOTE
CD 篡改检测主要靠服务端权威。客户端只发送释放技能的请求,不能上报技能是否冷却完成。服务端保存每个玩家每个技能的 lastCastTime 和 cooldownDuration,收到请求后用服务端时间判断 serverNow >= lastCastTime + cooldownDuration,同时校验蓝量、状态、距离、目标合法性和消息序号。如果 CD 未完成就拒绝,并返回剩余 CD;如果客户端频繁提前请求,就记录日志、累计嫌疑分,严重时踢下线或封禁。
一句话记忆
客户端 CD 是给玩家看的,服务端 CD 才是能不能放技能的账本。
伤害篡改如何检测?
核心原则
伤害篡改检测的核心是:客户端不能上报“我造成了多少伤害”,只能上报“我想释放哪个技能、打哪个目标”。真正的命中、暴击、伤害、扣血,必须由服务端重新计算。
为什么不能信客户端伤害
客户端可能被改内存、改代码、改包。
比如客户端发:
skillId = 3
targetId = 1008
damage = 999999
isCritical = true如果服务器直接相信,就会出现秒杀、刷副本、刷排行榜。
所以正确做法是:客户端发技能请求,服务端自己查配置和状态。
服务端要校验什么
收到攻击请求后,服务端检查:
技能是否存在
技能 CD 是否结束
蓝量或能量是否足够
角色是否沉默、眩晕、死亡
目标是否存在并且存活
攻击距离是否足够
朝向或碰撞是否合理
请求是否重复
攻击频率是否异常然后服务端自己计算:
攻击者攻击力
目标防御力
技能倍率
Buff / Debuff
暴击概率
随机种子
伤害浮动
元素克制
减伤护盾最终扣血也由服务端执行。
C# 简化代码
c
public sealed class DamageValidator // 定义伤害校验器。
{ // 类开始。
public int CalcDamage(int attack, int defense, float skillRate) // 根据服务端权威数据计算伤害。
{ // 方法开始。
int baseDamage = attack - defense; // 用攻击力减防御力得到基础伤害。
if (baseDamage < 1) // 如果基础伤害小于 1。
{ // 判断开始。
baseDamage = 1; // 至少造成 1 点伤害,避免出现负数伤害。
} // 判断结束。
int finalDamage = (int)(baseDamage * skillRate); // 根据技能倍率计算最终伤害。
return finalDamage; // 返回服务端计算出来的最终伤害。
} // 方法结束。
public bool CheckClientDamage(int clientDamage, int serverDamage, int tolerance) // 对比客户端上报伤害和服务端伤害。
{ // 方法开始。
int diff = clientDamage - serverDamage; // 计算客户端伤害和服务端伤害的差值。
if (diff < 0) // 如果差值是负数。
{ // 判断开始。
diff = -diff; // 把差值转成正数,方便比较绝对差距。
} // 判断结束。
if (diff > tolerance) // 如果差值超过允许容差。
{ // 判断开始。
return false; // 判定为异常伤害,需要拒绝、记录日志或累计嫌疑分。
} // 判断结束。
return true; // 差值在容差内,认为没有明显异常。
} // 方法结束。
} // 类结束。更稳的项目做法
严格项目里,客户端伤害字段甚至不参与结算,只能用于日志对比。
客户端伤害:仅用于表现预测或异常分析
服务端伤害:真正用于扣血和结算也就是说,即使客户端上报 damage = 500,服务器算出来是 120,最终也应该按 120 扣血。
异常处理
不要只看一次异常就封号。
可以这样处理:
第一次异常:拒绝本次请求
多次异常:记录战斗日志
持续异常:累计嫌疑分
严重异常:踢下线或封禁
关键战斗:保存回放或输入日志战斗日志要记录:
玩家 ID
技能 ID
目标 ID
服务端时间
客户端 Tick
攻击者属性
目标属性
Buff 列表
服务端计算伤害
客户端上报伤害
异常原因面试高分回答
CAUTION
伤害篡改不能靠客户端自检,必须服务端权威。客户端只发送释放技能的请求,比如技能 ID、目标 ID、输入 Tick,不能决定伤害值。服务端收到后校验 CD、蓝量、角色状态、目标状态、攻击距离、碰撞或命中条件,再根据服务端保存的属性、技能配置、Buff、随机数计算最终伤害并扣血。如果客户端上报了伤害值,只能作为日志或预测对比,不能参与结算。对于频繁异常的玩家,可以记录战斗日志、累计嫌疑分,严重时踢下线或封禁。
一句话记忆
客户端只负责说“我要打谁”,服务器负责判断“能不能打”和“到底打多少”。
重放攻击是什么?
核心解释
重放攻击就是:攻击者把以前抓到的一个“合法请求包”保存下来,然后之后再次发送给服务器,试图让服务器重复执行同一件事。
它和 Replay 回放系统不是一回事。Replay 回放系统是正常功能;重放攻击是安全问题。
举个游戏例子
玩家正常领取一次奖励:
ClaimReward rewardId = 7服务器第一次收到时,这是合法的。
攻击者如果把这个包保存下来,之后又发一次:
ClaimReward rewardId = 7如果服务器没有做防重放,就可能又发一次奖励。
类似风险还有:
重复领取邮件附件
重复领取任务奖励
重复提交购买请求
重复释放关键技能
重复提交战斗结算
重复上传排行榜成绩为什么签名不一定够
很多人会误以为:“我给包做签名了,就安全了。”
但重放攻击的问题是:旧包本来就是真的,签名当然也是真的。
所以只验签名是不够的。 服务器还要判断:
这个请求是不是已经处理过
这个请求是不是太旧
这个请求是不是属于当前会话
这个业务状态是否允许再次执行怎么防重放
常见做法有 4 个。
请求唯一 ID:
requestId = 10086服务端记录已经处理过的 requestId。 如果同一个 requestId 又来了,直接拒绝。
时间戳:
timestamp = 客户端请求时间服务端只接受一个时间窗口内的请求。太旧的请求直接丢弃。
Nonce:
nonce = 一次性随机数每个 nonce 只能用一次,用过就作废。
业务幂等:
rewardId = 7 已经领取过
所以不能再次领取即使网络层漏了,业务层也要挡住。
服务端应该怎么判断
比如领取奖励请求,服务端应该检查:
签名是否正确
token 是否有效
requestId 是否处理过
timestamp 是否过期
nonce 是否用过
这个奖励是否已经领取
玩家是否满足领取条件只有全部通过,才真正发奖励。
C# 简化代码
c
using System; // 引入 DateTimeOffset 等基础类型。
using System.Collections.Generic; // 引入 HashSet 集合。
public sealed class ReplayAttackGuard // 定义一个防重放检查器。
{ // 类开始。
private readonly HashSet<string> usedRequestIds = new HashSet<string>(); // 保存已经处理过的请求 ID。
private readonly long allowedWindowSeconds = 30; // 设置请求允许的时间窗口为 30 秒。
public bool IsValid(string requestId, long clientTimestamp, long serverNow) // 判断请求是否通过防重放校验。
{ // 方法开始。
if (usedRequestIds.Contains(requestId)) // 如果这个请求 ID 已经被处理过。
{ // 判断开始。
return false; // 说明这是重复请求,直接拒绝。
} // 判断结束。
long age = serverNow - clientTimestamp; // 计算请求距离服务器当前时间过去了多久。
if (age < 0 || age > allowedWindowSeconds) // 如果请求来自未来,或者已经太旧。
{ // 判断开始。
return false; // 说明时间不合法,直接拒绝。
} // 判断结束。
usedRequestIds.Add(requestId); // 校验通过后记录这个请求 ID,防止之后再次使用。
return true; // 返回请求有效。
} // 方法结束。
} // 类结束。面试高分回答
NOTE
重放攻击是指攻击者截获或保存一次合法请求,然后在之后重复发送,让服务器误以为这是新的请求。它的危险点在于这个包本身可能签名正确、格式正确,所以不能只靠签名防护。常见防护方式是给关键请求加 requestId、nonce、timestamp 和签名,服务端记录已处理请求,限制时间窗口,并且在业务层做幂等校验,比如奖励是否已经领取、订单是否已经处理。这样即使旧包被再次发送,服务器也能识别并拒绝。
一句话记忆
重放攻击就是“拿旧包再用一次”;防它要靠唯一编号、时间窗口、一次性随机数和业务幂等。
协议加密有什么用?
核心解释
协议加密的作用是保护“网络传输中的消息”。它主要防止别人抓包后直接看懂内容、分析协议、简单改包或伪造请求。
它能防什么
能防抓包看明文。
比如没有加密时,别人可能看到:
token
账号信息
聊天内容
技能 ID
物品 ID
协议字段加密后,中间人看到的是密文,不容易直接分析。
它也能提高改包成本。 如果配合 HMAC、签名或 AEAD 认证标签,别人改了包里的一个字节,服务端就能发现校验失败。
它不能防什么
协议加密不能让客户端变可信。
因为消息到了客户端以后,总要被解密。解密后,内存里仍然有明文。外挂可以 Hook 函数、改内存、改逻辑。
所以协议加密防的是:
路上被偷看
路上被简单修改
协议被低成本分析不是防:
客户端内存修改
本地逻辑 Hook
伤害篡改
CD 篡改
移速外挂这些仍然要靠服务端权威校验。
项目里常见做法
TCP 可以用:
TLS
HTTPSUDP 可以用:
DTLS
QUIC
应用层加密算法上不要自己发明,优先用成熟方案,比如:
AES-GCM
ChaCha20-Poly1305
HMAC-SHA256其中 AES-GCM、ChaCha20-Poly1305 属于 AEAD,既能加密,也能做完整性认证。
面试高分回答
TIP
协议加密主要用于保护网络传输安全,防止抓包看到明文、分析协议字段、直接改包或伪造请求。实际项目里不能只做加密,还要配合完整性校验,比如 HMAC 或 AEAD,保证消息被篡改后能被发现。同时还要加序号、时间戳、nonce 来防重放攻击。但协议加密不能让客户端本身变可信,因为客户端最终要解密消息,内存和逻辑仍可能被 Hook 或修改,所以伤害、CD、金币、掉落这些关键逻辑仍然必须由服务端权威校验。
一句话记忆
协议加密保护的是“路上的包”,不是保护“玩家手里的客户端”。
资源加密能防什么?
协议加密有什么用
协议加密保护的是“网络传输过程”。简单说,就是客户端和服务器之间发消息时,不让中间抓包的人直接看懂、修改或伪造消息。
它主要能防:
抓包直接看到明文
Token、账号信息、聊天内容泄漏
协议字段被轻易分析
普通改包
部分伪造请求但要注意:只“加密”还不够,最好还要有完整性校验,比如 MAC、签名、AEAD。否则别人虽然看不懂内容,但仍可能篡改密文导致异常。
常见做法:
TCP / HTTPS:TLS
UDP:DTLS、QUIC,或者应用层加密
算法:AES-GCM、ChaCha20-Poly1305不要自己乱设计加密算法,项目里优先用成熟方案。
协议加密防不住什么
协议加密不能让客户端变可信。
因为消息到了客户端以后,总要被解密。解密后,内存里还是明文,外挂可以 Hook、改内存、改逻辑。
所以协议加密防的是“路上被偷看、被简单篡改”,不是防所有外挂。
关键规则仍然要服务端权威:
伤害
金币
掉落
CD
排行榜
战斗结算
背包变化资源加密能防什么
资源加密保护的是本地文件和下载资源,比如:
AssetBundle
配置表
Lua 脚本
剧情文本
模型
贴图
音频
热更新资源包它主要能防:
普通玩家直接解包看资源
直接修改配置表作弊
提前偷看剧情、角色、关卡
资源被竞品或工具轻松提取
热更包被简单篡改比如玩家直接改本地配置:
attack = 100
改成
attack = 999999资源加密加签名后,客户端加载前可以发现文件被改过。
资源加密防不住什么
资源加密也不是绝对安全。
因为客户端最终要使用资源,就必须在某个时刻解密。 只要资源能被客户端加载,理论上就可能被运行时抓取、内存 Dump、Hook 解密函数,或者从显存里提取。
所以资源加密的作用更像是:
防普通解包
防低成本复制
防简单篡改
提高破解成本不是:
永远不被破解
彻底防外挂
完全保护美术资源面试高分回答
IMPORTANT
协议加密主要保护网络传输过程,防止抓包看到明文、分析协议、简单改包和伪造请求。实际项目里除了加密,还要配合完整性校验、签名、序号、时间戳和防重放机制。资源加密主要保护本地资源文件,比如 AssetBundle、配置表、Lua 脚本,防止普通解包、直接篡改配置和低成本盗取资源。但无论协议加密还是资源加密,都只能提高攻击成本,不能让客户端绝对可信;关键结算逻辑仍然必须放服务端。
一句话记忆
协议加密防“路上被看和改”,资源加密防“本地被扒和改”,但最终安全还是靠服务端权威。
如何防止本地存档被改?
核心原则
本地存档不能“绝对防改”,只能提高修改成本,并且在加载时发现篡改。真正重要的数据,比如金币、充值、排行榜、背包资产,必须放服务端。
常见做法
保存时不要直接写明文 JSON:
存档数据
加版本号
加用户 ID
加 saveIndex
加时间戳
加密
计算 HMAC 签名
写入文件读取时反过来:
读取文件
校验 HMAC
校验用户 ID
校验版本
校验 saveIndex 是否倒退
通过后再解密和反序列化
失败就拒绝读取或使用备份档HMAC 是干什么的
HMAC 可以发现文件有没有被改。
比如存档原本是:
gold = 100
level = 5玩家改成:
gold = 999999
level = 99内容一变,重新计算出来的 HMAC 就和原来的对不上,加载时就能发现异常。
加密是干什么的
加密主要让玩家不能直接看懂存档内容。
但注意,加密不等于绝对安全,因为客户端最终要解密存档,密钥也在客户端某处。高手仍然可能逆向拿到密钥。
所以:
HMAC:发现有没有被改
加密:让内容不容易被看懂
服务端:保证真正重要数据可信还要防回滚存档
有些玩家不是改存档,而是备份旧存档,然后反复覆盖回来。
比如抽卡前备份,抽不好就恢复旧存档。
可以加:
saveIndex
serverSaveVersion
timestamp每次保存递增 saveIndex。如果加载时发现本地 saveIndex 比云端或安全存储里的小,就说明可能被回滚。
面试高分回答
TIP
本地存档防篡改不能做到绝对安全,因为文件和客户端都在玩家设备上。常见做法是保存时对存档内容加密,并用 HMAC 或签名做完整性校验;加载时先验签,签名不一致就拒绝读取。同时存档里要带用户 ID、版本号、时间戳和递增的 saveIndex,用来防止跨账号复制和回滚旧档。对于金币、充值、排行榜、背包资产这类关键数据,不能只依赖本地存档,必须由服务端保存和校验,本地最多做缓存。
一句话记忆
本地存档靠加密提高修改成本,靠签名发现篡改,靠服务端保证真正安全。
反作弊和用户体验如何平衡?
核心原则
反作弊和用户体验的平衡点是:关键结果必须服务端权威,但检测和处罚要分级,不能一上来就重度扫描、强踢、封号。反作弊保护公平,用户体验保护留存,两边都不能丢。
为什么要平衡
如果反作弊太弱:
外挂扩散
排行榜失真
PVP 不公平
经济系统被刷爆
正常玩家流失如果反作弊太激进:
误封正常玩家
低端机卡顿
耗电发热
频繁弹窗影响体验
权限索取过多引发不信任所以最好的方案不是“越严越好”,而是“关键处严格,体验处克制”。
推荐做法
第一层:服务端权威。
这些必须由服务端决定:
伤害
金币
掉落
背包
交易
排行榜
技能 CD
战斗结算
任务奖励客户端可以预测和表现,但不能决定最终结果。
第二层:客户端辅助检测。
客户端可以检测一些明显异常:
内存异常
速度异常
资源完整性异常
频率异常
环境异常但客户端检测结果最好作为证据,不要单独作为最终封禁依据。
第三层:风险评分。
不要一次异常就封号,而是累计证据:
一次轻微异常:记录日志
多次异常:增加嫌疑分
持续异常:限制行为或强制校验
严重异常:踢下线、封禁、人工复核第四层:分级处罚。
比如:
低风险:只记录,不打扰玩家
中风险:纠偏、限频、提示网络异常
高风险:踢下线、封禁、人工复核这样可以减少误伤。
体验上要注意
检测不要放在主线程频繁跑,避免卡顿。
不要每次一点异常就弹窗。
弱网、掉帧、低端机要给容差。
检测数据要尽量小,不要上传大量无关隐私信息。
被处罚要有日志证据和申诉能力。
面试高分回答
TIP
我认为反作弊和用户体验要分层平衡。首先,关键数据必须服务端权威,比如伤害、资产、排行榜、技能 CD 和战斗结算,不能相信客户端结果。其次,客户端反作弊只做辅助检测和证据采集,尽量低频、异步、低打扰,避免影响帧率和耗电。处罚策略不能过于粗暴,应该按风险分级:低风险记录日志,中风险纠偏或限频,高风险再踢下线、封禁或人工复核。这样既能保护公平性,又能降低误封和体验损耗。
一句话记忆
反作弊不是越狠越好,而是关键结果服务端权威,检测低打扰,处罚按证据分级。