Skip to content

安全与反作弊

预计阅读 32 分钟
版本信息
最后更新:2026-07-09 · 适用基础库:≥ 3.9.0(最低兼容 ≥ 2.30.0) · 微信开发者工具:稳定版 1.06+
本章定位

本章面向已经上线或准备上线商业化微信小游戏的独立开发者。你不需要成为安全专家,但必须建立正确的安全观:客户端不可信、服务端权威、关键操作留痕。读完本章,你将能够为一人团队搭建一套“足够好”的反作弊最小可行架构。

小游戏运行在用户设备上,代码、资源、本地存储都可以被读取和修改。与 Web 前端不同,小游戏里的分数、金币、排行榜排名直接关联广告收入和内购收入,一次成功作弊就可能让真实付费玩家流失,甚至导致广告平台判定异常流量而限流。

本章从客户端防护、服务端权威、数据校验、支付安全、排行榜反作弊五个维度,给出可直接落地的方案。


1. 为什么小游戏安全不可忽视

独立小游戏常见的安全威胁有三类:

威胁类型典型后果影响对象
分数/排行榜作弊排行榜失去公信力,正常玩家流失竞技类、超休闲
货币/道具复制内购收入被稀释,经济系统崩溃含 IAP 或广告激励
广告刷量被广告平台判定作弊,账号封禁、收益扣减广告变现为主的游戏

前端工程师容易低估这些风险,因为 Web 应用的数据大多在服务端,客户端只是展示层。但小游戏里,一局游戏的完整逻辑往往跑在客户端,分数产生在本地,再上报给服务器。这意味着“客户端算出的分数”天然不可信。


2. 客户端防护:让破解成本高于收益

客户端无法做到绝对安全,但可以通过提高破解成本让大多数作弊者放弃。

2.1 代码混淆

微信小游戏代码包上传后会经过平台处理,但仍建议在上传前进行额外混淆:

typescript
// vite / rollup 构建配置示例
export default defineConfig({
  build: {
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
      mangle: {
        properties: {
          regex: /^_/, // 混淆以下划线开头的私有属性
        },
      },
      format: {
        comments: false,
      },
    },
  },
})

混淆后,变量名、私有方法名变得无意义,静态分析难度会显著增加。

2.2 资源加密

图片、音频、配置表可以直接从小游戏包中提取。对于需要保护的美术资源或关卡配置:

typescript
// 简单 XOR 加密示例(仅增加提取成本,不能替代服务端校验)
class AssetDecryptor {
  constructor(private key: Uint8Array) {}

  decrypt(buffer: ArrayBuffer): ArrayBuffer {
    const src = new Uint8Array(buffer)
    const out = new Uint8Array(src.length)
    for (let i = 0; i < src.length; i++) {
      out[i] = src[i] ^ this.key[i % this.key.length]
    }
    return out.buffer
  }
}
资源加密的最佳实践
  • 密钥不要硬编码在代码里,可以从服务器动态获取短期密钥。
  • 加密主要保护“美术资产不被直接盗用”,对反作弊帮助有限。
  • 关卡配置、掉落概率等关键数值建议放在服务端,客户端只保留展示数据。

2.3 基础完整性校验

客户端可以做一些轻量级自保护,例如检测常见调试器、校验自身文件哈希:

typescript
function isDebuggerAttached(): boolean {
  // 微信开发者工具及真机调试时通常为 true
  return __WXDEBUG__ || false
}

function checkRuntimeIntegrity(): boolean {
  if (isDebuggerAttached()) {
    console.warn('调试环境,跳过完整性校验')
    return true
  }
  // 生产环境下可结合服务端下发预期哈希做校验
  return true
}
前端对比:Web 前端安全 vs 小游戏安全
维度Web 前端微信小游戏
代码可见性用户可查看源码代码包可解包,逻辑暴露
本地存储LocalStorage / IndexedDBwx.setStorage + 文件系统
调试门槛浏览器 DevTools 即可开发者工具 + 真机调试
关键逻辑位置通常服务端大量逻辑在客户端
反作弊重点XSS、CSRF、接口防刷内存修改、存档篡改、协议伪造

关键差异:Web 前端可以把“不可信”隔离在展示层,小游戏必须把“不可信”假设推进到游戏核心逻辑。


3. 服务端权威状态:永远不信任客户端

核心思路是:游戏过程可以在客户端计算,但关键结果必须由服务端确认或重算

3.1 分数提交的最小服务端实现

以下是一个 Node.js / Express 风格的分数提交接口示例,包含签名校验、时间戳校验、nonce 去重和异常检测:

typescript
import express from 'express'
import crypto from 'crypto'

const app = express()
const SERVER_SECRET = process.env.SCORE_SECRET! // 服务端密钥,绝不暴露给客户端

app.use(express.json())

app.post('/api/score/submit', authenticateUser, async (req, res) => {
  const { score, level, seed, replayHash, signature, timestamp, nonce } = req.body

  // 1. 基础格式与范围校验
  if (!Number.isInteger(score) || score < 0 || score > 1_000_000) {
    return res.status(400).json({ ok: false, error: 'invalid_score' })
  }

  // 2. 重放攻击防护:时间戳窗口 + nonce 幂等
  const now = Date.now()
  if (Math.abs(now - timestamp) > 60_000) {
    return res.status(400).json({ ok: false, error: 'request_expired' })
  }
  const isNonceFresh = await redis.set(`nonce:${nonce}`, '1', 'NX', 'EX', 120)
  if (!isNonceFresh) {
    return res.status(400).json({ ok: false, error: 'duplicate_request' })
  }

  // 3. HMAC-SHA256 签名校验
  const payload = `${score}:${level}:${seed}:${timestamp}:${nonce}`
  const expectedSig = crypto.createHmac('sha256', SERVER_SECRET).update(payload).digest('hex')

  const sigBuf = Buffer.from(signature, 'utf8')
  const expectedBuf = Buffer.from(expectedSig, 'utf8')
  if (sigBuf.length !== expectedBuf.length) {
    return res.status(403).json({ ok: false, error: 'invalid_signature' })
  }
  if (!crypto.timingSafeEqual(sigBuf, expectedBuf)) {
    return res.status(403).json({ ok: false, error: 'invalid_signature' })
  }

  // 4. 异常检测
  const history = await db.getRecentScores(req.user.openid, 24)
  if (isAnomaly(score, history)) {
    await db.flagScore(req.user.openid, score, 'anomaly')
    return res.status(202).json({ ok: false, error: 'score_under_review' })
  }

  // 5. 写入排行榜
  await db.updateLeaderboard(req.user.openid, score)
  const rank = await db.getRank(req.user.openid)

  return res.json({ ok: true, rank })
})

3.2 客户端签名上报

客户端在提交分数前,使用服务端下发的短期密钥计算签名:

typescript
class ScoreSubmitter {
  private secret: string

  constructor(secret: string) {
    this.secret = secret
  }

  async submit(score: number, level: number, seed: string) {
    const timestamp = Date.now()
    const nonce = generateNonce(16)
    const replayHash = await this.computeReplayHash()

    const message = `${score}:${level}:${seed}:${timestamp}:${nonce}`
    const signature = await hmacSHA256(message, this.secret)

    return wx.request({
      url: 'https://your-server.com/api/score/submit',
      method: 'POST',
      data: {
        score,
        level,
        seed,
        replayHash,
        signature,
        timestamp,
        nonce,
      },
    })
  }

  private async computeReplayHash(): Promise<string> {
    // 对关键事件序列做摘要,供服务端做 replay 校验
    const events = GameRecorder.getInstance().getEventDigest()
    return sha256(events.join('|'))
  }
}

4. 数据校验:从输入到分数

4.1 输入校验 checklist

校验项说明示例
类型与范围分数必须是整数且在合理区间0 <= score <= 1_000_000
时间窗口防止重放攻击请求时间戳与服务器时间差 < 60 秒
nonce 幂等同一请求只处理一次Redis SET NX EX
签名校验确保字段未被篡改HMAC-SHA256
业务一致性分数与关卡、种子是否匹配根据 seed 和 replayHash 验证

4.2 基础异常检测

typescript
function isAnomaly(score: number, history: number[]): boolean {
  if (history.length < 3) return false

  const maxHistorical = Math.max(...history)
  const avg = history.reduce((a, b) => a + b, 0) / history.length

  // 单日涨幅超过历史最高分 4 倍
  if (score > maxHistorical * 4) return true

  // 超过历史平均分 10 倍且绝对值较高
  if (score > avg * 10 && score > 1000) return true

  // 超过游戏理论上限
  const THEORETICAL_MAX = 999_999
  if (score > THEORETICAL_MAX) return true

  return false
}

4.3 Replay 验证思路

对于核心玩法简单的游戏,可以让客户端上传“关键事件序列”,服务端按相同规则重算分数:

typescript
interface GameEvent {
  type: 'jump' | 'collect' | 'hit'
  frame: number
  param?: number
}

function replayScore(seed: string, events: GameEvent[]): number {
  const rng = createRNG(seed)
  let score = 0

  for (const event of events) {
    if (event.type === 'collect') score += 10
    if (event.type === 'hit') score -= 50
  }

  return Math.max(0, score)
}

Replay 验证成本较高,适合锦标赛、周榜结算等高价值场景。


5. 虚拟支付签名校验

微信小游戏使用米大师(Midas)虚拟支付。与苹果 IAP 不同,米大师支付目前主要依赖客户端 wx.requestMidasPaymentsuccess 回调,服务器侧没有直接到微信平台的订单验证接口。这意味着支付安全更依赖开发者自己的服务端校验。

5.1 支付流程

客户端下单 → 服务端创建订单并返回 orderId → 客户端调起支付
→ 支付成功回调 → 客户端上报服务端 → 服务端校验并发货

5.2 服务端发货校验

typescript
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })

exports.main = async (event) => {
  const { openid, orderId, productId, paidAmount, clientSignature } = event

  // 1. 幂等:订单是否已处理
  const exists = await db
    .collection('orders')
    .doc(orderId)
    .get()
    .catch(() => null)
  if (exists) return { code: 'DUPLICATE_ORDER' }

  // 2. 签名校验(防止客户端伪造字段)
  const expectedSig = hmacSHA256(`${openid}:${orderId}:${productId}:${paidAmount}`, SERVER_SECRET)
  if (!timingSafeEqual(clientSignature, expectedSig)) {
    return { code: 'INVALID_SIGNATURE' }
  }

  // 3. 金额与商品配置校验
  const product = PRODUCT_CONFIG[productId]
  if (!product || product.price !== paidAmount) {
    await logSuspicious(openid, 'price_mismatch', event)
    return { code: 'PRICE_MISMATCH' }
  }

  // 4. 若平台提供回调或订单查询,则二次确认
  const verifyResult = await queryMidasOrder(orderId)
  if (verifyResult && verifyResult.status !== 'SUCCESS') {
    return { code: 'VERIFY_FAILED' }
  }

  // 5. 原子发货
  await db.collection('orders').add({
    data: {
      orderId,
      openid,
      productId,
      paidAmount,
      status: 'paid',
      createdAt: db.serverDate(),
    },
  })
  await grantVirtualGoods(openid, product)

  return { code: 'SUCCESS' }
}
支付安全 checklist
  • 订单号由服务端生成,客户端只负责透传。
  • 商品价格以服务端配置为准,拒绝客户端上报的价格。
  • 发货操作必须幂等,同一订单号只发一次货。
  • 对 price_mismatch、重复订单等异常做日志和人工复核。

6. 排行榜反作弊

排行榜是最容易被攻击的模块之一。除了服务端分数校验外,还需要针对排行榜做专项防护。

6.1 限流与频率控制

typescript
const LEADERBOARD_SUBMIT_COOLDOWN = 60_000 // 每分钟最多提交一次

async function canSubmitScore(openid: string): Promise<boolean> {
  const last = await redis.get(`score_submit:${openid}`)
  if (last && Date.now() - Number(last) < LEADERBOARD_SUBMIT_COOLDOWN) {
    return false
  }
  await redis.set(`score_submit:${openid}`, String(Date.now()), 'EX', 120)
  return true
}

6.2 排行榜异常检测

typescript
interface LeaderboardEntry {
  openid: string
  score: number
  submittedAt: number
}

function detectLeaderboardAnomaly(entries: LeaderboardEntry[]): string[] {
  const sorted = [...entries].sort((a, b) => b.score - a.score)
  const top10 = sorted.slice(0, 10)
  const suspicious: string[] = []

  // 前 10 名分数异常高于中位数
  const median = sorted[Math.floor(sorted.length / 2)].score
  for (const entry of top10) {
    if (entry.score > median * 100) {
      suspicious.push(entry.openid)
    }
  }

  // 短时间集中上榜
  const now = Date.now()
  for (const entry of sorted) {
    if (now - entry.submittedAt < 300_000 && entry.score > median * 50) {
      suspicious.push(entry.openid)
    }
  }

  return [...new Set(suspicious)]
}

6.3 榜单更新策略

策略说明适用场景
实时写入提交后立即更新榜单低并发、休闲小游戏
延迟归档先写入待审核队列,定时归档高价值排行榜
周期结算每小时/每天结算一次锦标赛、周榜

7. 常见作弊向量与缓解方案

作弊方式原理缓解方案
内存修改器(如 GameGuardian)运行时修改分数、金币数值服务端权威计算 + 异常检测
变速齿轮 / 加速器修改游戏时间流速服务端校验时间戳、心跳间隔
存档篡改修改本地 Storage 或文件敏感数据加密、关键数据放服务端
协议重放重复发送有效请求nonce 幂等、时间窗口
自动化脚本模拟点击、自动玩游戏行为检测、验证码、人机校验
修改器注入Frida、LLDB 等动态调试提高破解成本、服务端校验
没有银弹

以上任何一种缓解方案都只能提高作弊成本。对于独立开发者,优先把精力投入到“服务端校验”和“异常检测”上,而不是追求客户端绝对不可破解。


8. 微信生态特有的安全能力

8.1 加密本地存储

微信小游戏基础库 2.16.0+ 提供了 wx.getUserCryptoManager,可在微信安全环境中做 AES 对称加密,适合保护本地敏感缓存:

typescript
// ⚠️ wx.getUserCryptoManager 要求基础库 >= 2.17.3
if (!wx.getUserCryptoManager) {
  console.warn('当前基础库不支持 getUserCryptoManager,请升级基础库或使用降级方案')
} else {
  const cryptoManager = wx.getUserCryptoManager()

  // ⚠️ encrypt() 的 data 和 key 参数必须是 ArrayBuffer,不是 string!
  async function encryptLocalData(plainText: string, keyString: string): Promise<string> {
    // 将字符串转为 ArrayBuffer
    const encoder = new TextEncoder()
    const dataBuffer = encoder.encode(plainText)
    const keyBuffer = encoder.encode(keyString)

    return new Promise((resolve, reject) => {
      cryptoManager.encrypt({
        data: dataBuffer.buffer as ArrayBuffer, // ArrayBuffer
        key: keyBuffer.buffer as ArrayBuffer, // ArrayBuffer
        success: (res) => resolve(res.encryptedData),
        fail: reject,
      })
    })
  }
}

8.2 云函数做权威逻辑

微信云开发(CloudBase)可以让独立开发者零运维地拥有服务端能力。把分数校验、支付发货、排行榜写入放到云函数里:

typescript
// 云函数:权威分数写入
const cloud = require('wx-server-sdk')
cloud.init()

exports.main = async (event, context) => {
  const wxContext = cloud.getWXContext()
  const openid = wxContext.OPENID

  // 在这里做完整的校验和写入
  return await processScoreSubmission(openid, event)
}

8.3 开放数据域的限制

开放数据域是微信为了保护社交关系链数据而设计的沙箱环境:

  • 无法访问网络,不能向主域发送消息。
  • 只能使用 sharedCanvas 做 2D 渲染。
  • 主域拿不到原始关系链数据,只能拿到渲染后的图像。

这意味着开放数据域天然适合做“只读展示型”排行榜,但无法替代服务端排行榜。对于需要防作弊的竞技排行榜,仍需把核心数据放在自己的服务器或云数据库中。


9. 一人团队最小可行安全架构

对于独立开发者,安全投入必须聚焦在“高杠杆”环节。推荐以下最小可行架构:

客户端
  ├── 代码混淆 + 资源基础加密
  ├── 本地敏感数据使用 cryptoManager 加密
  └── 关键操作上报服务端

服务端 / 云函数
  ├── 所有分数提交走服务端校验(签名 + nonce + 时间窗)
  ├── 排行榜写入限流 + 异常检测
  ├── 支付发货:订单幂等 + 金额校验 + 签名校验
  └── 日志记录:可疑请求、price_mismatch、异常涨幅
独立开发者安全优先级
  1. 第一优先级:服务端权威(分数、金币、道具数量由服务端最终确认)。
  2. 第二优先级:支付发货安全(订单幂等、金额校验)。
  3. 第三优先级:排行榜异常检测与限流。
  4. 第四优先级:客户端混淆与资源加密。

不要颠倒顺序。客户端加固做得再好,也挡不住有心人的逆向分析。


📝 课后练习

练习 1:设计服务端权威分数提交接口

题目: 为你正在做的一款跑酷小游戏设计一个 /api/score/submit 接口。列出必须校验的字段、异常检测规则,以及如何处理被判定为异常的分数。

参考答案要点:

  • 校验字段:score(整数、范围)、levelseedtimestamp(时间窗)、nonce(幂等)、signature(HMAC)。
  • 异常规则:单日涨幅超过历史最高分 4 倍、超过理论上限、提交频率过高。
  • 异常处理:不直接拒绝,而是标记为“待审核”,写入可疑日志,人工复核后再决定是否上榜。
练习 2:支付发货流程推演

题目: 描述米大师支付从客户端下单到服务端发货的完整流程,并指出其中最容易被伪造的环节。

参考答案要点:

  • 流程:服务端创建订单 → 客户端调起支付 → 支付成功 → 客户端上报服务端 → 服务端校验签名、金额、订单状态 → 发货。
  • 最脆弱环节:客户端上报支付结果。攻击者可以伪造成功回调请求,因此服务端必须校验订单号、金额、签名,并做幂等控制。
练习 3:排行榜反作弊策略

题目: 你的游戏上线了周榜,发现前几名分数明显异常。设计一个自动检测与处理方案。

参考答案要点:

  • 检测:计算榜单中位数,标记高于中位数 100 倍的条目;标记 5 分钟内集中上榜且分数极高的用户。
  • 处理:移入“待审核榜”,不直接展示;触发人工复核;复核通过后替换正式榜。
  • 预防:提交限流、服务端权威分数、replay 验证高价值榜单。

📚 相关阅读

用心学习,持续实践