Q19: 如何防止消息重放攻击?
核心结论
防重放不能只靠传输层,也不能只靠业务层。
一套完整方案通常至少包含两层:
- 传输或会话层防止旧包、重复包再次被接受
- 业务层保证关键操作即使重复到达,也不会重复生效
如果只做前者,交易、发奖、扣费这类操作仍然可能出问题;如果只做后者,登录、会话、鉴权链路又会暴露在明显风险下。
一、什么是重放攻击
重放攻击不是篡改消息,而是把一条曾经合法的消息再次发送。
典型前提包括:
- 攻击者能截获请求
- 消息本身在格式上仍然合法
- 服务端缺少“这条消息已经过期或已处理”的判断
它的危险之处在于,消息内容可能完全真实,因此单纯校验格式、校验字段、甚至校验签名,都不一定足够。
二、真正危险的场景是什么
1. 登录或会话恢复
如果登录令牌、重连令牌、换服票据可以被重复利用,攻击者可能在令牌有效期内伪造一次“合法重连”。
2. 资产类操作
例如:
- 购买
- 转账
- 发奖
- 领取邮件附件
- 使用兑换码
这类操作一旦被重放,直接影响资产正确性。
3. 战斗或玩法指令
高频操作虽然单笔价值未必高,但如果同一条攻击或施法指令被重复接受,也会导致:
- 重复伤害
- 重复位移
- 非法状态叠加
4. 后台或内部服务调用
不要只盯客户端。服务间消息、GM 指令、补偿脚本、支付回调,同样存在重放风险,而且一旦出错影响通常更大。
三、为什么“有签名”仍然不够
签名解决的是“消息有没有被篡改”,不解决“这条合法消息是不是又被发了一次”。
例如一条已经签名的发奖请求:
- 第一次发送时合法
- 第二次原样发送时,签名仍然合法
如果没有额外的时效性或唯一性校验,服务端依然可能照常执行。
所以防重放至少要回答两个问题:
- 这条消息是不是新的
- 这条操作是不是已经处理过
四、常用技术分别解决什么问题
1. 时间戳
时间戳适合解决“过期消息”问题。
常见做法是:
- 消息带发送时间
- 服务端只接受一个短时间窗口内的请求
它的优点是简单,缺点也很明显:
- 依赖时钟同步
- 无法阻止窗口内的重复发送
所以时间戳通常只能做第一层过滤。
2. Sequence
会话序列号适合处理“同一连接或同一会话里的重复包、乱序包”。
常见做法是:
- 每个会话维护递增序列
- 服务端记录已接受到的最大序列或滑动窗口
- 重复序列直接丢弃
它更适合:
- 实时战斗输入
- 移动指令
- 高频低价值消息
但它不擅长解决跨连接、跨重试、跨服务链路的重复业务提交。
3. Nonce
Nonce 本质上是一条消息的一次性随机标识。
服务端在短窗口内记录已经见过的 nonce,再次出现就拒绝。
它适合:
- 登录票据
- 敏感接口调用
- 短周期的一次性消息
缺点是要维护存储或缓存窗口。
4. 幂等操作 ID
这是业务层最关键的一层。
对于购买、领奖、转账这类操作,应该让请求带一个业务唯一 ID,例如:
order_idrequest_idoperation_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
- 各类支付回调与幂等设计实践资料
