Java 防重放攻击实战:从原理到落地

简介: 重放攻击(Replay Attack)是一种常见的网络攻击手段,攻击者通过截取网络中传输的合法请求数据(如API调用参数、令牌等),然后在未授权的情况下重复发送该请求,以达到欺骗服务器、获取非法利益的目的。在Java开发中,重放攻击多发生在HTTP接口(尤其是RESTful API)、RPC调用、分布式系统通信等场景。要防御重放攻击,核心思路是让每个合法请求都具备“唯一性”和“时效性”,使攻击者截取的旧请求无法被服务器正常处理。

不知道你有没有遇到过这种情况:

线上支付接口突然出现多笔重复扣款,用户投诉不断;下单接口被恶意重复调用,生成一堆无效订单;甚至有攻击者拦截正常请求,反复发送窃取用户数据……

这些令人头大的问题,大概率都是「重放攻击」在搞鬼。

作为 Java 后端开发者,防重放攻击是必备技能,尤其是对接支付、订单、用户中心等核心接口时,少了这层防护,系统就像裸奔一样危险。

今天就给大家带来一套 落地性拉满的 Java 防重放攻击解决方案,从原理拆解到代码实现,再到分布式场景优化,一步步帮你筑牢系统安全防线。编写 Java 重放攻击落地方案教程 (1).png

一、前置知识:什么是重放攻击?

重放攻击(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("&timestamp=").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 分布式缓存和幂等设计,就能应对绝大多数场景。

这套方案已经在多个生产项目中落地验证,大家可以根据自己的业务场景稍作修改直接复用。如果你的项目有特殊需求,或者在实现过程中遇到问题,欢迎在评论区留言讨论~

相关文章
|
6天前
|
数据采集 人工智能 安全
|
16天前
|
云安全 监控 安全
|
2天前
|
机器学习/深度学习 人工智能 前端开发
构建AI智能体:七十、小树成林,聚沙成塔:随机森林与大模型的协同进化
随机森林是一种基于决策树的集成学习算法,通过构建多棵决策树并结合它们的预测结果来提高准确性和稳定性。其核心思想包括两个随机性:Bootstrap采样(每棵树使用不同的训练子集)和特征随机选择(每棵树分裂时只考虑部分特征)。这种方法能有效处理大规模高维数据,避免过拟合,并评估特征重要性。随机森林的超参数如树的数量、最大深度等可通过网格搜索优化。该算法兼具强大预测能力和工程化优势,是机器学习中的常用基础模型。
264 155
|
3天前
|
机器学习/深度学习 人工智能 前端开发
构建AI智能体:六十九、Bootstrap采样在大模型评估中的应用:从置信区间到模型稳定性
Bootstrap采样是一种通过有放回重抽样来评估模型性能的统计方法。它通过从原始数据集中随机抽取样本形成多个Bootstrap数据集,计算统计量(如均值、标准差)的分布,适用于小样本和非参数场景。该方法能估计标准误、构建置信区间,并量化模型不确定性,但对计算资源要求较高。Bootstrap特别适合评估大模型的泛化能力和稳定性,在集成学习、假设检验等领域也有广泛应用。与传统方法相比,Bootstrap不依赖分布假设,在非正态数据中表现更稳健。
200 105
|
9天前
|
SQL 自然语言处理 调度
Agent Skills 的一次工程实践
**本文采用 Agent Skills 实现整体智能体**,开发框架采用 AgentScope,模型使用 **qwen3-max**。Agent Skills 是 Anthropic 新推出的一种有别于mcp server的一种开发方式,用于为 AI **引入可共享的专业技能**。经验封装到**可发现、可复用的能力单元**中,每个技能以文件夹形式存在,包含特定任务的指导性说明(SKILL.md 文件)、脚本代码和资源等 。大模型可以根据需要动态加载这些技能,从而扩展自身的功能。目前不少国内外的一些框架也开始支持此种的开发方式,详细介绍如下。
688 5
|
12天前
|
人工智能 自然语言处理 API
一句话生成拓扑图!AI+Draw.io 封神开源组合,工具让你的效率爆炸
一句话生成拓扑图!next-ai-draw-io 结合 AI 与 Draw.io,通过自然语言秒出架构图,支持私有部署、免费大模型接口,彻底解放生产力,绘图效率直接爆炸。
806 152

热门文章

最新文章