不知道你有没有遇到过这种情况:
线上支付接口突然出现多笔重复扣款,用户投诉不断;下单接口被恶意重复调用,生成一堆无效订单;甚至有攻击者拦截正常请求,反复发送窃取用户数据……
这些令人头大的问题,大概率都是「重放攻击」在搞鬼。
作为 Java 后端开发者,防重放攻击是必备技能,尤其是对接支付、订单、用户中心等核心接口时,少了这层防护,系统就像裸奔一样危险。
今天就给大家带来一套 落地性拉满的 Java 防重放攻击解决方案,从原理拆解到代码实现,再到分布式场景优化,一步步帮你筑牢系统安全防线。
一、前置知识:什么是重放攻击?
重放攻击(Replay Attack)是一种常见的网络攻击手段,攻击者通过截取网络中传输的合法请求数据(如API调用参数、令牌等),然后在未授权的情况下重复发送该请求,以达到欺骗服务器、获取非法利益的目的。例如:用户发起一笔转账请求,攻击者截取该请求数据后重复发送,可能导致用户重复转账。
在Java开发中,重放攻击多发生在HTTP接口(尤其是RESTful API)、RPC调用、分布式系统通信等场景。要防御重放攻击,核心思路是让每个合法请求都具备“唯一性”和“时效性”,使攻击者截取的旧请求无法被服务器正常处理。
举个通俗的例子:你在超市付款时,付款信息被坏人偷偷录了下来,他拿着这段录下来的信息,去超市重复“付款”操作,把你账户里的钱转走——这就是生活中的“重放攻击”。
在计算机网络中,攻击者不需要破解请求内容,只要把截取到的合法请求原封不动地重新发送,就能达到攻击目的。比如:
拦截支付请求,重复发送导致多扣款
截取登录成功后的令牌,重复调用用户接口获取数据
恶意重复调用下单接口,占用系统资源
二、核心防御思路:三大核心要素
Java项目中防御重放攻击,主流方案是结合「时间戳+随机数(Nonce)+签名机制」,三者协同作用:
时间戳(Timestamp):保证请求的时效性。服务器规定请求的有效时间窗口(如5分钟),若请求的时间戳与服务器当前时间差值超过窗口阈值,则直接拒绝。
随机数(Nonce):保证请求的唯一性。每个请求生成一个全局唯一的随机数,服务器记录已处理过的随机数,若收到重复的随机数则拒绝请求。
签名(Signature):防止请求参数被篡改。客户端将时间戳、随机数、请求参数等关键信息,结合密钥进行加密生成签名;服务器接收请求后,使用相同的规则重新计算签名,若两次签名不一致则拒绝请求。
这三个机制组合起来,就能形成完整的防御闭环:时效性+唯一性+防篡改,彻底杜绝重放攻击。
三、实战落地:完整代码实现(直接复用)
下面是一套基于 Spring Boot 的完整实现方案,包含核心工具类、缓存管理、拦截器等,复制到项目里稍作修改就能用。
1. 先引入依赖(Maven)
需要用到工具类、缓存和 Redis 相关依赖,直接复制到 pom.xml 即可:
<!-- 工具类:生成Nonce、签名、时间处理 -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
<!-- Redis:分布式场景存储Nonce(生产必备) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 核心工具类:Nonce+时间戳+签名
封装了生成唯一随机串、验证时间戳、生成和验证签名的核心方法,密钥建议配置在 Nacos 等配置中心:
import org.apache.commons.codec.digest.HmacSHA256Utils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.Map;
import java.util.TreeMap;
/**
* 防重放攻击核心工具类
* 包含Nonce生成、时间戳验证、签名生成与验证
*/
public class ReplayAttackUtils {
// 签名密钥(生产环境务必配置在配置中心,不同用户可分配独立密钥)
private static final String SECRET_KEY = "your_secure_secret_key_123456";
// 请求有效期(5分钟,单位:毫秒)
private static final long REQUEST_EXPIRE_TIME = 5 * 60 * 1000L;
/**
* 生成32位唯一Nonce(随机串)
*/
public static String generateNonce() {
return RandomStringUtils.randomAlphanumeric(32);
}
/**
* 验证时间戳是否过期(允许±1分钟偏差,避免时区/时钟问题)
*/
public static boolean isTimestampExpired(long timestamp) {
long currentTime = System.currentTimeMillis();
return Math.abs(currentTime - timestamp) > REQUEST_EXPIRE_TIME;
}
/**
* 生成请求签名:参数按字典序排序 + Nonce + Timestamp + 密钥 进行HmacSHA256签名
* @param params 业务参数(不含signature、nonce、timestamp)
* @param nonce 唯一随机串
* @param timestamp 时间戳
* @return 签名串
*/
public static String generateSignature(Map<String, String> params, String nonce, long timestamp) {
// 1. 参数按字典序排序,避免参数顺序导致签名不一致
TreeMap<String, String> sortedParams = new TreeMap<>(params);
// 2. 拼接参数串
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
if (StringUtils.isNotBlank(entry.getValue())) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
}
sb.append("nonce=").append(nonce).append("×tamp=").append(timestamp);
// 3. HmacSHA256签名并转小写(安全性比MD5高,避免碰撞)
return HmacSHA256Utils.hmacSha256(sb.toString(), SECRET_KEY).toLowerCase();
}
/**
* 验证签名是否合法
*/
public static boolean verifySignature(Map<String, String> params, String nonce, long timestamp, String sign) {
if (StringUtils.isAnyBlank(nonce, sign) || timestamp <= 0) {
return false;
}
String generateSign = generateSignature(params, nonce, timestamp);
return generateSign.equals(sign);
}
}
3. Nonce 缓存管理:防止重复使用
生产环境用 Redis 支持分布式场景,本地用 Guava Cache 兜底,避免 Redis 网络问题影响服务:
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* Nonce管理器:记录已使用的Nonce,防止重复请求
*/
@Component
public class NonceManager {
// 本地缓存(单机场景):过期时间=请求有效期+1分钟,避免缓存溢出
private final Cache<String, Boolean> nonceLocalCache = CacheBuilder.newBuilder()
.expireAfterWrite(6, TimeUnit.MINUTES)
.maximumSize(100000)
.build();
// Redis缓存(分布式场景,生产必备)
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 检查Nonce是否已使用
*/
public boolean isNonceUsed(String nonce) {
// 1. 优先查Redis(分布式场景下保证一致性)
Boolean exists = stringRedisTemplate.hasKey("nonce:" + nonce);
if (exists != null && exists) {
return true;
}
// 2. 本地缓存兜底(应对Redis临时不可用)
return nonceLocalCache.getIfPresent(nonce) != null;
}
/**
* 标记Nonce为已使用(验证通过后再标记,避免无效请求占用缓存)
*/
public void markNonceUsed(String nonce) {
// 1. 写入Redis,设置过期时间
stringRedisTemplate.opsForValue().set("nonce:" + nonce, "USED", 6, TimeUnit.MINUTES);
// 2. 写入本地缓存
nonceLocalCache.put(nonce, true);
}
}
4. 拦截器:统一验证所有核心请求
用 Spring MVC 拦截器统一校验,不用在每个接口里重复写验证逻辑,只需要配置需要防护的接口路径即可:
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* 防重放攻击拦截器:对核心接口统一进行验证
*/
@Component
public class ReplayAttackInterceptor implements HandlerInterceptor {
@Resource
private NonceManager nonceManager;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取请求参数
String nonce = request.getParameter("nonce");
String timestampStr = request.getParameter("timestamp");
String sign = request.getParameter("signature");
long timestamp = 0;
try {
timestamp = Long.parseLong(timestampStr);
} catch (NumberFormatException e) {
response.setStatus(403);
response.getWriter().write("无效的时间戳");
return false;
}
// 2. 验证时间戳是否过期
if (ReplayAttackUtils.isTimestampExpired(timestamp)) {
response.setStatus(403);
response.getWriter().write("请求已过期,请重新发起");
return false;
}
// 3. 验证Nonce是否已使用(防止重复请求)
if (nonceManager.isNonceUsed(nonce)) {
response.setStatus(403);
response.getWriter().write("重复请求,已拒绝");
return false;
}
// 4. 验证签名(防止参数被篡改)
Map<String, String> params = new HashMap<>();
Enumeration<String> parameterNames = request.getParameterNames();
while (parameterNames.hasMoreElements()) {
String key = parameterNames.nextElement();
// 排除signature、nonce、timestamp,避免循环验证
if (!"signature".equals(key) && !"nonce".equals(key) && !"timestamp".equals(key)) {
params.put(key, request.getParameter(key));
}
}
if (!ReplayAttackUtils.verifySignature(params, nonce, timestamp, sign)) {
response.setStatus(403);
response.getWriter().write("签名无效,请求可能被篡改");
return false;
}
// 5. 标记Nonce为已使用(验证通过后再标记,减少无效缓存)
nonceManager.markNonceUsed(nonce);
return true;
}
}
5. 注册拦截器:指定需要防护的接口
配置拦截器,只对核心接口(如下单、支付)进行防护,避免对所有接口造成性能影响:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
private ReplayAttackInterceptor replayAttackInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 对核心业务接口添加拦截防护
registry.addInterceptor(replayAttackInterceptor)
.addPathPatterns("/api/order/**") // 订单相关接口
.addPathPatterns("/api/pay/**") // 支付相关接口
.addPathPatterns("/api/user/modify"); // 用户信息修改接口
}
}
6. 客户端请求示例(模拟)
客户端需要按规则生成 Nonce、时间戳和签名,再拼接参数发送请求:
import java.util.HashMap;
import java.util.Map;
/**
* 客户端请求示例:生成合法请求参数
*/
public class ClientDemo {
public static void main(String[] args) {
// 1. 构造业务参数
Map<String, String> params = new HashMap<>();
params.put("orderId", "O20251224001");
params.put("amount", "100.00");
params.put("userId", "U123456");
// 2. 生成Nonce和时间戳
String nonce = ReplayAttackUtils.generateNonce();
long timestamp = System.currentTimeMillis();
// 3. 生成签名
String signature = ReplayAttackUtils.generateSignature(params, nonce, timestamp);
// 4. 拼接请求参数(包含nonce、timestamp、signature)
params.put("nonce", nonce);
params.put("timestamp", String.valueOf(timestamp));
params.put("signature", signature);
// 5. 发送请求(此处省略HTTP请求代码)
System.out.println("合法请求参数:" + params);
}
}
四、进阶优化:应对分布式与高并发
上面的基础方案适用于大部分场景,但如果是分布式系统或高并发场景,还需要做以下优化:
1. Redis 原子操作防并发
分布式场景下,多个节点同时检查 Nonce 可能出现并发问题,用 Redis 的 SETNX 命令(不存在则设置)实现原子操作:
// 优化NonceManager的markNonceUsed方法
public boolean markNonceUsedAtomic(String nonce) {
// SETNX:不存在则设置成功,返回true;已存在则返回false
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent("nonce:" + nonce, "USED", 6, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(success)) {
return false; // Nonce已存在,标记失败
}
// 写入本地缓存
nonceLocalCache.put(nonce, true);
return true;
}
2. 密钥动态化与分级
不要用固定密钥!建议:
为每个用户分配独立密钥,避免一个密钥泄露影响所有用户
密钥存储在配置中心,支持动态更新,无需重启服务
3. 核心接口必加幂等设计
防重放攻击是“基础防护”,核心接口(如下单、支付)还需要结合业务实现幂等,避免极端情况下的重复操作。比如用订单号作为唯一标识:
@PostMapping("/api/order/create")
public Result createOrder(@RequestParam String orderId, @RequestParam String userId) {
// 1. 分布式锁防并发
String lockKey = "lock:order:" + orderId;
Boolean locked = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, "LOCK", 30, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(locked)) {
return Result.fail("订单处理中,请稍后再试");
}
try {
// 2. 检查订单是否已存在(幂等核心)
Order order = orderService.getByOrderId(orderId);
if (order != null) {
return Result.success(order); // 重复请求直接返回结果
}
// 3. 正常创建订单
orderService.createOrder(orderId, userId);
return Result.success();
} finally {
// 释放锁
stringRedisTemplate.delete(lockKey);
}
}
4. HTTPS 必须配合使用
上面的方案能防重放,但如果请求被拦截,攻击者虽然不能重放,但可能窃取参数。因此,生产环境必须启用 HTTPS,防止请求被拦截和篡改。
五、常见问题与避坑指南
客户端时间偏差导致请求过期? 允许 ±1 分钟的时间偏差,或让客户端请求前先同步服务端时间。
Nonce 缓存占用内存过多? 合理设置过期时间(比请求有效期多1分钟即可),并限制缓存最大容量。
签名算法选哪种? 优先用 HmacSHA256,避免 MD5(易被碰撞);敏感场景可用 RSA 非对称加密。
测试环境调试麻烦? 可在测试环境临时关闭拦截器,或放宽时间限制,但生产环境必须严格验证。
最后总结
Java 防重放攻击的核心就是「时效性+唯一性+防篡改」,通过 Nonce+时间戳+签名的组合方案,再配合 Redis 分布式缓存和幂等设计,就能应对绝大多数场景。
这套方案已经在多个生产项目中落地验证,大家可以根据自己的业务场景稍作修改直接复用。如果你的项目有特殊需求,或者在实现过程中遇到问题,欢迎在评论区留言讨论~