Article

Unity WebGL 小游戏实战 14:安全边界与反作弊基础(客户端可信最小化)

路线阶段:Unity WebGL 小游戏实战第 14 章。
本章目标:明确“客户端不可信”前提下的最小安全体系,避免核心经济和排行榜被轻易污染。

学习目标

完成本章后,你应该能做到:

  1. 定义客户端与服务端的可信边界。
  2. 识别高风险路径(成绩提交、奖励发放、活动领奖)。
  3. 建立幂等、签名、异常检测三层防护。
  4. 把风控事件接入埋点与运维告警。

WebGL 安全现实

需要明确:

  1. 客户端逻辑可被逆向与篡改。
  2. 本地时间、请求参数都可能伪造。
  3. 仅靠前端校验无法防作弊。

因此策略是:关键结果必须可验证,关键奖励必须可追踪。

可信边界划分

客户端可做

  1. 交互体验校验(减少误操作)。
  2. 基础异常拦截(明显非法输入)。
  3. 日志与证据上报。

服务端必须做

  1. 奖励最终发放决策。
  2. 排行榜成绩可信度判断。
  3. 活动领奖次数与时间窗校验。

高风险接口清单

  1. submit_score
  2. claim_reward
  3. event_claim
  4. purchase_item

这些接口必须具备:

  1. 幂等 token
  2. 请求签名/时间戳
  3. 风险评分

幂等机制

public sealed class IdempotencyTokenBuilder
{
    public static string Build(string playerId, string action, string nonce)
    {
        return playerId + "|" + action + "|" + nonce;
    }
}

服务端保存最近 token,重复请求直接拒绝或返回相同结果。

请求签名

public sealed class RequestSigner
{
    public static string Sign(string payloadJson, string ts, string secret)
    {
        var raw = payloadJson + "|" + ts + "|" + secret;
        return Sha256(raw);
    }

    private static string Sha256(string text)
    {
        using (var sha = System.Security.Cryptography.SHA256.Create())
        {
            var bytes = System.Text.Encoding.UTF8.GetBytes(text);
            var hash = sha.ComputeHash(bytes);
            return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
        }
    }
}

注意:WebGL 客户端里的 secret 不能视为真正安全,仅作为低成本筛查。

成绩异常检测

public sealed class ScoreRiskEvaluator
{
    public int Evaluate(ScoreSubmitPayload p)
    {
        var risk = 0;

        if (p.Score <= 0) risk += 30;
        if (p.KillCount > 200000) risk += 40;
        if (p.DurationSec <= 0 || p.DurationSec > 36000) risk += 20;
        if (p.MaxDps > 999999) risk += 40;

        if (p.ReplayHashMismatch) risk += 50;
        if (p.ClientBuildBlacklisted) risk += 80;

        return risk;
    }
}

[Serializable]
public sealed class ScoreSubmitPayload
{
    public string PlayerId;
    public int StageId;
    public int Score;
    public int KillCount;
    public int DurationSec;
    public int MaxDps;

    public bool ReplayHashMismatch;
    public bool ClientBuildBlacklisted;
}

策略:

  1. risk >= 80:直接拒绝。
  2. 40 <= risk < 80:进入人工复核/延迟上榜。
  3. < 40:正常入榜。

奖励风控

public sealed class RewardRiskGuard
{
    private readonly Dictionary<string, int> _claimCounter = new Dictionary<string, int>(256);

    public bool CanClaim(string playerId, string rewardKey, long nowMs)
    {
        var bucket = playerId + "|" + rewardKey + "|" + DayBucket(nowMs);
        int c;
        if (!_claimCounter.TryGetValue(bucket, out c))
        {
            _claimCounter[bucket] = 1;
            return true;
        }

        if (c >= 3)
        {
            return false;
        }

        _claimCounter[bucket] = c + 1;
        return true;
    }

    private static long DayBucket(long ms)
    {
        return ms / (24L * 3600L * 1000L);
    }
}

回放佐证

对于高价值成绩:

  1. 要求提交回放摘要 hash。
  2. 服务端抽样复跑回放。
  3. 若关键指标偏差大,标记可疑。

黑名单与熔断

最小能力:

  1. 黑名单设备指纹/账号。
  2. 异常峰值时临时关闭高风险入口(如双倍奖励广告)。
  3. 自动触发安全告警。

安全埋点

新增事件:

  1. risk_score_submit
  2. risk_reward_claim
  3. risk_action_blocked
  4. risk_manual_review

与前面系统联动

  1. 排行榜系统:成绩上榜前走风险评估。
  2. 活动系统:领奖请求加入幂等与频控。
  3. 广告系统:奖励发放走统一防重逻辑。
  4. 发布运维:风险事件进入告警看板。

WebGL 注意点

  1. 前端混淆只能提高门槛,不是安全方案。
  2. 客户端检测与服务端判定必须分层。
  3. 安全策略升级要支持远程配置开关。

验收清单

  1. 重放同一领奖请求不会重复发奖。
  2. 异常成绩会被标记并阻断上榜。
  3. 高风险事件可被监控系统捕获。
  4. 风险策略配置可热更新。

常见坑

坑 1:把签名逻辑当核心防线

客户端密钥可泄露。签名只是筛查,不是终极防护。

坑 2:风控只做“拒绝”,不做留痕

无法复盘和迭代。每次拦截必须记录上下文。

坑 3:策略写死代码

应支持远程调参,否则响应慢。

本月作业

实现“排行榜防刷一期”:

  1. 成绩提交接入风险评分。
  2. 高风险成绩延迟上榜并打标。
  3. 输出一周风控命中报告(命中率/误杀率)。

下一章进入 Unity WebGL 小游戏实战 15:版本长线演进与技术债治理(可持续迭代收官)。