Apollo 技术文档Apollo 技术文档
指南
  • 架构概述
  • BigWorld 架构深度解析
  • BigWorld 进程架构与玩家生命周期
  • AOI九宫格系统详解
  • AOI广播与消息去重
  • Base 模块
  • Core 模块
  • Runtime 模块
  • Data 模块
  • Network 模块
  • /modules/actor.html
  • Game 模块
  • BigWorld 模块
服务器应用
API 参考
QA
GitHub
指南
  • 架构概述
  • BigWorld 架构深度解析
  • BigWorld 进程架构与玩家生命周期
  • AOI九宫格系统详解
  • AOI广播与消息去重
  • Base 模块
  • Core 模块
  • Runtime 模块
  • Data 模块
  • Network 模块
  • /modules/actor.html
  • Game 模块
  • BigWorld 模块
服务器应用
API 参考
QA
GitHub
  • MMORPG 架构 QA

Q19: 如何防止消息重放攻击?

核心结论

防重放不能只靠传输层,也不能只靠业务层。

一套完整方案通常至少包含两层:

  • 传输或会话层防止旧包、重复包再次被接受
  • 业务层保证关键操作即使重复到达,也不会重复生效

如果只做前者,交易、发奖、扣费这类操作仍然可能出问题;如果只做后者,登录、会话、鉴权链路又会暴露在明显风险下。

一、什么是重放攻击

重放攻击不是篡改消息,而是把一条曾经合法的消息再次发送。

典型前提包括:

  • 攻击者能截获请求
  • 消息本身在格式上仍然合法
  • 服务端缺少“这条消息已经过期或已处理”的判断

它的危险之处在于,消息内容可能完全真实,因此单纯校验格式、校验字段、甚至校验签名,都不一定足够。

二、真正危险的场景是什么

1. 登录或会话恢复

如果登录令牌、重连令牌、换服票据可以被重复利用,攻击者可能在令牌有效期内伪造一次“合法重连”。

2. 资产类操作

例如:

  • 购买
  • 转账
  • 发奖
  • 领取邮件附件
  • 使用兑换码

这类操作一旦被重放,直接影响资产正确性。

3. 战斗或玩法指令

高频操作虽然单笔价值未必高,但如果同一条攻击或施法指令被重复接受,也会导致:

  • 重复伤害
  • 重复位移
  • 非法状态叠加

4. 后台或内部服务调用

不要只盯客户端。服务间消息、GM 指令、补偿脚本、支付回调,同样存在重放风险,而且一旦出错影响通常更大。

三、为什么“有签名”仍然不够

签名解决的是“消息有没有被篡改”,不解决“这条合法消息是不是又被发了一次”。

例如一条已经签名的发奖请求:

  • 第一次发送时合法
  • 第二次原样发送时,签名仍然合法

如果没有额外的时效性或唯一性校验,服务端依然可能照常执行。

所以防重放至少要回答两个问题:

  • 这条消息是不是新的
  • 这条操作是不是已经处理过

四、常用技术分别解决什么问题

1. 时间戳

时间戳适合解决“过期消息”问题。

常见做法是:

  • 消息带发送时间
  • 服务端只接受一个短时间窗口内的请求

它的优点是简单,缺点也很明显:

  • 依赖时钟同步
  • 无法阻止窗口内的重复发送

所以时间戳通常只能做第一层过滤。

2. Sequence

会话序列号适合处理“同一连接或同一会话里的重复包、乱序包”。

常见做法是:

  • 每个会话维护递增序列
  • 服务端记录已接受到的最大序列或滑动窗口
  • 重复序列直接丢弃

它更适合:

  • 实时战斗输入
  • 移动指令
  • 高频低价值消息

但它不擅长解决跨连接、跨重试、跨服务链路的重复业务提交。

3. Nonce

Nonce 本质上是一条消息的一次性随机标识。

服务端在短窗口内记录已经见过的 nonce,再次出现就拒绝。

它适合:

  • 登录票据
  • 敏感接口调用
  • 短周期的一次性消息

缺点是要维护存储或缓存窗口。

4. 幂等操作 ID

这是业务层最关键的一层。

对于购买、领奖、转账这类操作,应该让请求带一个业务唯一 ID,例如:

  • order_id
  • request_id
  • operation_id

服务端处理原则是:

  • 第一次看到该 ID,执行操作并记录结果
  • 再次看到同一 ID,不重复执行,只返回已存在结果

这一步不是“安全附加项”,而是资产系统的基础能力。

五、传输层防重放和业务幂等不是一回事

这两层常常被混淆,但边界必须清楚。

1. 传输层防重放

关注的是:

  • 重复包
  • 乱序包
  • 过期包
  • 会话合法性

常见手段:

  • 时间戳
  • 序列号
  • Nonce
  • 会话令牌
  • 消息签名

2. 业务层幂等

关注的是:

  • 这笔业务是不是已经成功执行过
  • 重试时是否会再次扣费、发货、发奖
  • 服务重启或超时重试后是否还能保持结果一致

常见手段:

  • 业务唯一 ID
  • 状态机约束
  • 去重表
  • 唯一索引
  • 结果缓存

两层都要做,尤其是资产和支付链路。

六、不同类型消息应采用不同策略

1. 高频实时消息

例如移动、朝向、普通战斗输入。

更合适的组合通常是:

  • 会话序列号
  • 窗口去重
  • 权威服重算

这类消息追求低成本和低延迟,不适合每条都做沉重的持久化去重。

2. 中价值操作

例如技能释放、交互、切图、组队确认。

通常可以采用:

  • 时间戳或短期 nonce
  • 会话序列
  • 服务端状态合法性校验

3. 高价值操作

例如购买、转账、发奖、发货、支付回调。

必须至少具备:

  • 强签名或可信鉴权
  • 时效控制
  • 业务唯一 ID
  • 幂等落库

如果缺少最后一项,前面几层仍然不够稳。

七、断线重连场景尤其容易出问题

很多系统在正常请求链路上做了防重放,但在重连流程里留下缺口。

典型风险包括:

  • 重连 token 可多次使用
  • 老会话和新会话同时有效
  • 客户端重发最后几条请求时缺少去重
  • 网关重试和业务重试叠加,造成双执行

因此重连体系通常还需要:

  • 单次使用的重连票据
  • 明确的会话切换与旧连接作废
  • 断线期间请求的重提交流程设计
  • 业务操作的最终幂等

八、工程上比较稳妥的组合

一个常见的实用方案是:

  • 登录与重连:短期 token + 时间戳 + nonce + 签名
  • 实时指令:会话序列号 + 窗口去重 + 权威重算
  • 资产操作:业务唯一 ID + 幂等存储 + 签名校验
  • 服务回调:来源鉴权 + 重放窗口 + 唯一流水号

再配合:

  • 异常日志
  • 攻击频率监控
  • 可追溯审计记录

这样才能在发现问题时快速止损。

九、几个常见误区

1. 用 HTTPS/TLS 就不用防重放

不对。TLS 能保护传输通道,但不能替代业务幂等,也不能自动帮你处理应用层重复请求。

2. 用了序列号就万无一失

不对。序列号通常只在单连接、单会话范围内有效。只要发生重连、跨服务重试或业务异步补偿,就可能绕过这层保护。

3. 所有消息都做数据库去重

也不现实。高频实时消息这样做成本太高,通常应该按消息价值分层处理。

4. 只拦客户端,不拦内部调用

这是很危险的。很多事故不是外挂造成的,而是内部系统超时重试、补偿脚本重复执行、第三方回调重复通知导致的。

参考资料

  • OWASP, Replay Attack
  • RFC 8446, TLS 1.3
  • 各类支付回调与幂等设计实践资料
在 GitHub 上编辑此页
最后更新: 3/20/26, 6:06 AM
贡献者: cuihairu