Skip to content

反作弊与安全

客户端为什么不可信?

核心解释

客户端不可信,是因为客户端运行在玩家自己的机器上。玩家理论上可以改内存、改代码、Hook 函数、抓包改包、伪造请求、重放旧消息。所以服务器不能直接相信客户端上报的“结果”。

network-why-client-untrusted

为什么不可信

客户端代码可以被反编译、修改、注入。

客户端内存里的金币、血量、CD、位置都可能被改。

网络包也可能被抓包、篡改、重放。

本地时间和帧率也可能被改,形成加速、无 CD、瞬移之类的问题。

所以客户端说:

我打中了
我造成了 999999 伤害
我获得了 100000 金币
我已经通关了
我当前位置是终点

服务器都不能直接相信。

服务器应该信什么

服务器可以接收客户端的“意图”,但不能直接相信客户端的“结果”。

比如客户端可以说:

我按了移动键
我想释放技能 3
我点击了这个目标
我请求领取奖励

但服务器必须自己判断:

你是否真的有这个技能
技能 CD 是否好了
蓝量是否足够
距离是否够
目标是否还活着
是否在可攻击范围内
是否已经领取过奖励
这次请求是否重复

最终结果应该由服务器计算。

举个战斗例子

不安全做法:

客户端:我打中了怪物,造成 5000 伤害
服务器:好的,怪物扣血

这样很容易被改包。

安全做法:

客户端:我在 Tick 120 使用了技能 3,目标是怪物 5
服务器:检查技能 CD、距离、朝向、碰撞、目标状态
服务器:计算是否命中和造成多少伤害
服务器:广播最终结果

这里客户端只是提交操作,服务器才负责裁决。

项目里怎么设计

客户端负责:

输入采集
本地预测
动画表现
特效播放
UI 展示
弱提示

服务器负责:

角色真实属性
技能 CD
伤害计算
掉落结算
背包变化
任务进度
排行榜分数
战斗胜负

也就是说,客户端可以“先演”,但服务器必须“算账”。

常见防护手段

服务器权威
消息序号
ACK 和去重
请求限频
协议签名
时间戳校验
反重放
状态合法性校验
关键日志审计
异常行为检测

但要记住:加密、签名、防外挂都只是提高作弊成本,不应该替代服务器权威校验。

面试高分回答

NOTE

客户端不可信的根本原因是它运行在玩家可控环境里,代码、内存、输入和网络包都有可能被修改。服务器不能相信客户端上报的结果,比如伤害、金币、掉落、命中和结算,只能把客户端消息当成操作意图。正确做法是客户端发送输入或请求,服务器根据权威状态校验 CD、距离、资源、碰撞、权限和序号,然后由服务器计算最终结果并同步给客户端。客户端可以做预测和表现,但关键规则和资产变化必须由服务器裁决。

一句话记忆

客户端负责“我想做什么”,服务器负责“你能不能做,以及结果是什么”。

哪些逻辑必须放服务端?

核心原则

凡是影响公平、资产、进度、排名、结算的逻辑,都必须放服务端。客户端可以负责输入和表现,但不能决定最终结果。

network-server-authoritative-logic

必须放服务端的逻辑

账号和权限:

登录鉴权
Token 校验
账号状态
封禁状态
权限判断

资产和交易:

金币
钻石
道具
背包
商城购买
充值发货
邮件附件
交易系统
抽卡结果

战斗结算:

伤害计算
命中判定
暴击判定
技能 CD
蓝量消耗
Buff 生效
死亡判定
掉落归属
战斗胜负

成长和进度:

经验
等级
任务进度
成就
副本结算
每日奖励
活动奖励

公共竞争数据:

排行榜
匹配
赛季分
竞技场结算
公会贡献
拍卖行
世界 Boss 归属

随机结果:

抽卡
掉落
暴击
随机词条
宝箱奖励

这些如果放客户端,玩家改内存、改包、改时间,就可能直接刷资源、刷伤害、刷排名。

客户端可以做什么

客户端可以做:

输入采集
本地预测
动画表现
特效播放
音效播放
UI 展示
弱网提示
临时表现状态

比如玩家点击释放技能,客户端可以先播动作、先做特效,让手感更好。

但最终是否命中、造成多少伤害、消耗多少蓝、进入多少 CD,必须由服务器判断。

正确设计方式

客户端不要上报结果,而是上报意图。

不要这样:

客户端:我打中了,造成 5000 伤害
服务器:好的,扣血

应该这样:

客户端:我在 Tick 120 使用技能 3,目标是怪物 5
服务器:检查 CD、蓝量、距离、状态、碰撞、目标是否合法
服务器:计算命中和伤害
服务器:广播最终结果

这就是“客户端请求,服务器裁决”。

灰色区:移动逻辑

移动比较特殊。

为了手感,客户端通常会做本地预测:

客户端先移动
服务器校验速度和位置
发现异常就纠正

但服务器仍然要检查:

是否超速
是否穿墙
是否瞬移
是否无视控制状态
是否在合法区域

所以移动可以客户端先表现,但服务端必须有权威校验和纠偏。

面试高分回答

IMPORTANT

我会按影响范围来划分。凡是影响资产、公平、进度、排行榜和最终结算的逻辑,都必须放服务端,比如伤害、命中、技能 CD、掉落、背包、金币、任务、排行榜和交易。客户端只负责输入、预测和表现,不能直接决定结果。客户端发送的是“我想做什么”,比如释放技能或领取奖励;服务端根据权威状态校验条件,计算最终结果,保存数据并同步给客户端。这样可以防止改内存、改包、重放请求导致作弊。

一句话记忆

客户端负责“演出来”,服务器负责“算清楚”。

移速外挂如何检测?

核心原则

移速外挂检测的本质是:客户端说“我到了哪里”不能直接信,服务端要根据自己的时间、角色规则、地图合法性和历史轨迹判断这个移动是否合理。

network-speed-hack-detection

怎么检测

最基础的检测是距离除以时间:

实际移动距离 = 本次位置 - 上次位置
允许移动距离 = 最大速度 * 时间差 + 容差

如果:

实际移动距离 > 允许移动距离

就说明玩家可能超速。

但项目里不能只看这一条,因为网络抖动、丢包、冲刺、击退、传送、技能位移都会影响结果。

所以服务端一般要综合判断:

角色当前最大速度
是否有加速 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 必须由服务端保存、计算和裁决。

network-cooldown-tamper-detection

为什么客户端 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 篡改检测主要靠服务端权威。客户端只发送释放技能的请求,不能上报技能是否冷却完成。服务端保存每个玩家每个技能的 lastCastTimecooldownDuration,收到请求后用服务端时间判断 serverNow >= lastCastTime + cooldownDuration,同时校验蓝量、状态、距离、目标合法性和消息序号。如果 CD 未完成就拒绝,并返回剩余 CD;如果客户端频繁提前请求,就记录日志、累计嫌疑分,严重时踢下线或封禁。

一句话记忆

客户端 CD 是给玩家看的,服务端 CD 才是能不能放技能的账本。

伤害篡改如何检测?

核心原则

伤害篡改检测的核心是:客户端不能上报“我造成了多少伤害”,只能上报“我想释放哪个技能、打哪个目标”。真正的命中、暴击、伤害、扣血,必须由服务端重新计算。

network-damage-tamper-detection

为什么不能信客户端伤害

客户端可能被改内存、改代码、改包。

比如客户端发:

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 回放系统是正常功能;重放攻击是安全问题。

network-replay-attack-explained

举个游戏例子

玩家正常领取一次奖励:

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 和签名,服务端记录已处理请求,限制时间窗口,并且在业务层做幂等校验,比如奖励是否已经领取、订单是否已经处理。这样即使旧包被再次发送,服务器也能识别并拒绝。

一句话记忆

重放攻击就是“拿旧包再用一次”;防它要靠唯一编号、时间窗口、一次性随机数和业务幂等。

协议加密有什么用?

核心解释

协议加密的作用是保护“网络传输中的消息”。它主要防止别人抓包后直接看懂内容、分析协议、简单改包或伪造请求。

security-protocol-encryption-purpose

它能防什么

能防抓包看明文。

比如没有加密时,别人可能看到:

token
账号信息
聊天内容
技能 ID
物品 ID
协议字段

加密后,中间人看到的是密文,不容易直接分析。

它也能提高改包成本。 如果配合 HMAC、签名或 AEAD 认证标签,别人改了包里的一个字节,服务端就能发现校验失败。

它不能防什么

协议加密不能让客户端变可信。

因为消息到了客户端以后,总要被解密。解密后,内存里仍然有明文。外挂可以 Hook 函数、改内存、改逻辑。

所以协议加密防的是:

路上被偷看
路上被简单修改
协议被低成本分析

不是防:

客户端内存修改
本地逻辑 Hook
伤害篡改
CD 篡改
移速外挂

这些仍然要靠服务端权威校验。

项目里常见做法

TCP 可以用:

TLS
HTTPS

UDP 可以用:

DTLS
QUIC
应用层加密

算法上不要自己发明,优先用成熟方案,比如:

AES-GCM
ChaCha20-Poly1305
HMAC-SHA256

其中 AES-GCM、ChaCha20-Poly1305 属于 AEAD,既能加密,也能做完整性认证。

面试高分回答

TIP

协议加密主要用于保护网络传输安全,防止抓包看到明文、分析协议字段、直接改包或伪造请求。实际项目里不能只做加密,还要配合完整性校验,比如 HMAC 或 AEAD,保证消息被篡改后能被发现。同时还要加序号、时间戳、nonce 来防重放攻击。但协议加密不能让客户端本身变可信,因为客户端最终要解密消息,内存和逻辑仍可能被 Hook 或修改,所以伤害、CD、金币、掉落这些关键逻辑仍然必须由服务端权威校验。

一句话记忆

协议加密保护的是“路上的包”,不是保护“玩家手里的客户端”。

资源加密能防什么?

协议加密有什么用

协议加密保护的是“网络传输过程”。简单说,就是客户端和服务器之间发消息时,不让中间抓包的人直接看懂、修改或伪造消息。

security-protocol-and-resource-encryption

它主要能防:

抓包直接看到明文
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 脚本,防止普通解包、直接篡改配置和低成本盗取资源。但无论协议加密还是资源加密,都只能提高攻击成本,不能让客户端绝对可信;关键结算逻辑仍然必须放服务端。

一句话记忆

协议加密防“路上被看和改”,资源加密防“本地被扒和改”,但最终安全还是靠服务端权威。

如何防止本地存档被改?

核心原则

本地存档不能“绝对防改”,只能提高修改成本,并且在加载时发现篡改。真正重要的数据,比如金币、充值、排行榜、背包资产,必须放服务端。

security-local-save-tamper-prevention

常见做法

保存时不要直接写明文 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,用来防止跨账号复制和回滚旧档。对于金币、充值、排行榜、背包资产这类关键数据,不能只依赖本地存档,必须由服务端保存和校验,本地最多做缓存。

一句话记忆

本地存档靠加密提高修改成本,靠签名发现篡改,靠服务端保证真正安全。

反作弊和用户体验如何平衡?

核心原则

反作弊和用户体验的平衡点是:关键结果必须服务端权威,但检测和处罚要分级,不能一上来就重度扫描、强踢、封号。反作弊保护公平,用户体验保护留存,两边都不能丢。

security-anticheat-user-experience-balance

为什么要平衡

如果反作弊太弱:

外挂扩散
排行榜失真
PVP 不公平
经济系统被刷爆
正常玩家流失

如果反作弊太激进:

误封正常玩家
低端机卡顿
耗电发热
频繁弹窗影响体验
权限索取过多引发不信任

所以最好的方案不是“越严越好”,而是“关键处严格,体验处克制”。

推荐做法

第一层:服务端权威。

这些必须由服务端决定:

伤害
金币
掉落
背包
交易
排行榜
技能 CD
战斗结算
任务奖励

客户端可以预测和表现,但不能决定最终结果。

第二层:客户端辅助检测。

客户端可以检测一些明显异常:

内存异常
速度异常
资源完整性异常
频率异常
环境异常

但客户端检测结果最好作为证据,不要单独作为最终封禁依据。

第三层:风险评分。

不要一次异常就封号,而是累计证据:

一次轻微异常:记录日志
多次异常:增加嫌疑分
持续异常:限制行为或强制校验
严重异常:踢下线、封禁、人工复核

第四层:分级处罚。

比如:

低风险:只记录,不打扰玩家
中风险:纠偏、限频、提示网络异常
高风险:踢下线、封禁、人工复核

这样可以减少误伤。

体验上要注意

检测不要放在主线程频繁跑,避免卡顿。

不要每次一点异常就弹窗。

弱网、掉帧、低端机要给容差。

检测数据要尽量小,不要上传大量无关隐私信息。

被处罚要有日志证据和申诉能力。

面试高分回答

TIP

我认为反作弊和用户体验要分层平衡。首先,关键数据必须服务端权威,比如伤害、资产、排行榜、技能 CD 和战斗结算,不能相信客户端结果。其次,客户端反作弊只做辅助检测和证据采集,尽量低频、异步、低打扰,避免影响帧率和耗电。处罚策略不能过于粗暴,应该按风险分级:低风险记录日志,中风险纠偏或限频,高风险再踢下线、封禁或人工复核。这样既能保护公平性,又能降低误封和体验损耗。

一句话记忆

反作弊不是越狠越好,而是关键结果服务端权威,检测低打扰,处罚按证据分级。

文章评价

读完这篇,留下你的看法

暂无审核通过的评价。

登录账号后才能评价。

本站访客数0总站访问量0本页访问量0