在分布式系统中,“重放攻击” 是常见的安全威胁 —— 攻击者截获并重复发送合法请求(如支付、下单),可能导致用户重复支付、订单重复创建。防重放机制通过为请求添加 “时效性标识”,确保同一请求只能在有效时间内使用一次,成为抵御这类攻击的关键防线。
防重放的核心原理
防重放的核心是 “让请求具有唯一性和时效性”,实现逻辑:
- 客户端生成唯一随机数(Nonce)和当前时间戳(Timestamp)
- 将 Nonce、Timestamp、请求参数与密钥一起签名
- 服务端验证签名有效性、时间戳是否在有效期内(如 5 分钟)、Nonce 是否已使用过
- 验证通过则处理请求,并将 Nonce 存入 Redis 标记为已使用(设置与时间戳相同的过期时间)
实战实现方案
1. 签名 + 时间戳 + Nonce 三重验证
客户端实现(伪代码) :
// 生成随机数(32位UUID)
const nonce = uuid.v4().replace(/-/g, '');
// 生成时间戳(毫秒级)
const timestamp = Date.now();
// 请求参数
const params = { orderId: 123, amount: 100 };
// 拼接待签名字符串(参数按key排序)
const sortedParams = Object.keys(params).sort().map(key => `${key}=${params[key]}`).join('&');
const signStr = `${sortedParams}&nonce=${nonce}×tamp=${timestamp}&secret=${clientSecret}`;
// SHA256签名
const sign = sha256(signStr);
// 发送请求(参数+nonce+timestamp+sign)
axios.post('/api/pay', { ...params, nonce, timestamp, sign });
服务端实现(Java) :
@PostMapping("/api/pay")
public Result pay(@RequestBody PayRequest request) {
// 1. 验证时间戳是否在有效期内(5分钟)
long now = System.currentTimeMillis();
if (now - request.getTimestamp() > 5 * 60 * 1000) {
return Result.fail("请求已过期,请重新发起");
}
// 2. 验证Nonce是否已使用(Redis)
String nonceKey = "replay:nonce:" + request.getNonce();
Boolean isExist = redisTemplate.hasKey(nonceKey);
if (Boolean.TRUE.equals(isExist)) {
return Result.fail("重复请求,请不要重复提交");
}
// 3. 验证签名
String serverSign = generateSign(request.getParams(), request.getNonce(), request.getTimestamp(), serverSecret);
if (!serverSign.equals(request.getSign())) {
return Result.fail("签名无效");
}
// 4. 标记Nonce为已使用(设置5分钟过期)
redisTemplate.opsForValue().set(nonceKey, "1", 5, TimeUnit.MINUTES);
// 5. 处理支付逻辑
paymentService.process(request);
return Result.success("支付成功");
}
// 服务端生成签名(与客户端逻辑一致)
private String generateSign(Map<String, Object> params, String nonce, long timestamp, String secret) {
List<String> paramList = new ArrayList<>();
params.forEach((k, v) -> paramList.add(k + "=" + v));
Collections.sort(paramList);
String sortedParams = String.join("&", paramList);
String signStr = sortedParams + "&nonce=" + nonce + "×tamp=" + timestamp + "&secret=" + secret;
return DigestUtils.sha256Hex(signStr);
}
2. 令牌机制:一次性 Token 防重放
对于用户登录态下的请求,可结合 Token 实现防重放:
-
用户登录后,服务端生成临时 Token(如 JWT),包含用户 ID 和过期时间
-
客户端每次请求时携带 Token,服务端验证 Token 有效性
-
服务端处理请求后,使当前 Token 失效,返回新 Token 给客户端
-
客户端下次请求使用新 Token,确保 Token 只能用一次
优势:无需维护 Nonce,适合高频交互场景(如 WebSocket 通信)
防重放的注意事项
1. 时间同步与容差
- 客户端与服务端时间可能存在偏差,时间戳验证需设置合理容差(如 30 秒)
- 避免使用本地时间,建议客户端从服务端获取标准时间
2. Redis 高可用
- Nonce 和 Token 的存储依赖 Redis,需确保 Redis 主从同步和持久化配置,避免单点故障
- 可使用 Redis 集群 + 哨兵模式,确保服务可用性
3. 性能优化
- Nonce 生成可简化(如 16 位随机字符串),减少计算开销
- 对高频接口,可批量验证 Nonce(如一次验证多个 Nonce 是否存在)
避坑指南
-
不要忽略 HTTPS:防重放机制需配合 HTTPS 使用,避免参数和签名被中间人篡改
-
密钥定期轮换:客户端与服务端的密钥需定期更新,降低泄露风险
-
避免 Nonce 长度过短:短 Nonce 可能被暴力破解,建议至少 16 位随机字符串
防重放设计看似增加了系统复杂度,却是高安全级别接口(如支付、转账)的必备机制。它通过数学手段确保请求的唯一性,让攻击者即使截获请求,也无法从中获利,这正是后端安全设计 “防患于未然” 的体现。