RuoYi重复提交:防重复请求拦截器

RuoYi重复提交:防重复请求拦截器

【免费下载链接】RuoYi 🎉 基于SpringBoot的权限管理系统 易读易懂、界面简洁美观。 核心技术采用Spring、MyBatis、Shiro没有任何其它重度依赖。直接运行即可用 【免费下载链接】RuoYi 项目地址: https://gitcode.com/yangzongzhuan/RuoYi

引言:为什么需要防重复提交?

在Web应用开发中,重复提交(Repeat Submit)是一个常见但容易被忽视的问题。想象一下这样的场景:用户点击"提交订单"按钮后,由于网络延迟或页面响应慢,用户可能会重复点击,导致同一订单被创建多次。这不仅会造成数据冗余,还可能引发业务逻辑错误和用户体验问题。

RuoYi框架通过精心设计的防重复提交拦截器,为开发者提供了一套完整的解决方案。本文将深入解析RuoYi的防重复提交机制,从原理到实践,帮助你全面掌握这一重要功能。

防重复提交的核心架构

RuoYi的防重复提交机制采用经典的拦截器模式,结合自定义注解和Session存储,构建了一个灵活高效的防护体系。

mermaid

核心组件说明

组件名称类型职责描述
RepeatSubmitInterceptor抽象类防重复提交拦截器基类,定义核心拦截逻辑
SameUrlDataInterceptor实现类具体防重复提交实现,基于URL和参数比较
@RepeatSubmit注解标记需要防重复提交的方法
ResourcesConfig配置类注册拦截器到Spring MVC

深入源码解析

1. 自定义注解 @RepeatSubmit

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{
    /**
     * 间隔时间(ms),小于此时间视为重复提交
     * 默认值:5000毫秒(5秒)
     */
    public int interval() default 5000;

    /**
     * 提示消息
     * 默认值:"不允许重复提交,请稍后再试"
     */
    public String message() default "不允许重复提交,请稍后再试";
}

2. 抽象拦截器基类

@Component
public abstract class RepeatSubmitInterceptor implements HandlerInterceptor
{
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
    {
        if (handler instanceof HandlerMethod)
        {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
            if (annotation != null)
            {
                if (this.isRepeatSubmit(request, annotation))
                {
                    AjaxResult ajaxResult = AjaxResult.error(annotation.message());
                    ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
                    return false; // 拦截请求
                }
            }
            return true; // 放行请求
        }
        else
        {
            return true;
        }
    }

    public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception;
}

3. 具体实现:SameUrlDataInterceptor

@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
{
    public final String REPEAT_PARAMS = "repeatParams";
    public final String REPEAT_TIME = "repeatTime";
    public final String SESSION_REPEAT_KEY = "repeatData";

    @SuppressWarnings("unchecked")
    @Override
    public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception
    {
        // 获取当前请求参数和时间
        String nowParams = JSON.toJSONString(request.getParameterMap());
        Map<String, Object> nowDataMap = new HashMap<String, Object>();
        nowDataMap.put(REPEAT_PARAMS, nowParams);
        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());

        String url = request.getRequestURI();
        HttpSession session = request.getSession();
        
        Object sessionObj = session.getAttribute(SESSION_REPEAT_KEY);
        if (sessionObj != null)
        {
            Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
            if (sessionMap.containsKey(url))
            {
                Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
                if (compareParams(nowDataMap, preDataMap) && 
                    compareTime(nowDataMap, preDataMap, annotation.interval()))
                {
                    return true; // 判定为重复提交
                }
            }
        }
        
        // 更新Session数据
        Map<String, Object> sessionMap = new HashMap<String, Object>();
        sessionMap.put(url, nowDataMap);
        session.setAttribute(SESSION_REPEAT_KEY, sessionMap);
        return false; // 非重复提交
    }

    private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
    {
        String nowParams = (String) nowMap.get(REPEAT_PARAMS);
        String preParams = (String) preMap.get(REPEAT_PARAMS);
        return nowParams.equals(preParams);
    }

    private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval)
    {
        long time1 = (Long) nowMap.get(REPEAT_TIME);
        long time2 = (Long) preMap.get(REPEAT_TIME);
        return (time1 - time2) < interval;
    }
}

防重复提交的工作流程

mermaid

配置和使用指南

1. 拦截器配置

ResourcesConfig中自动配置拦截器:

@Configuration
public class ResourcesConfig implements WebMvcConfigurer
{
    @Autowired
    private RepeatSubmitInterceptor repeatSubmitInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry)
    {
        registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
    }
}

2. 在Controller中使用

@RestController
@RequestMapping("/order")
public class OrderController
{
    @PostMapping("/create")
    @RepeatSubmit(interval = 3000, message = "请勿重复提交订单")
    public AjaxResult createOrder(@RequestBody Order order)
    {
        // 订单创建业务逻辑
        orderService.createOrder(order);
        return AjaxResult.success("订单创建成功");
    }
}

3. 配置参数说明

参数类型默认值说明
intervalint5000防重复时间间隔(毫秒)
messageString"不允许重复提交,请稍后再试"重复提交时的提示消息

高级应用场景

场景1:不同业务的不同防重复策略

@RestController
public class BusinessController
{
    // 支付操作:2秒内防重复
    @PostMapping("/payment")
    @RepeatSubmit(interval = 2000, message = "支付请求处理中,请勿重复操作")
    public AjaxResult payment(@RequestBody PaymentRequest request) {
        // 支付逻辑
    }

    // 数据提交:5秒内防重复
    @PostMapping("/submitData")
    @RepeatSubmit(interval = 5000, message = "数据提交中,请稍后重试")
    public AjaxResult submitData(@RequestBody DataModel data) {
        // 数据提交逻辑
    }

    // 重要操作:10秒内防重复
    @PostMapping("/criticalOperation")
    @RepeatSubmit(interval = 10000, message = "重要操作执行中,请勿重复提交")
    public AjaxResult criticalOperation(@RequestBody OperationRequest request) {
        // 重要操作逻辑
    }
}

场景2:自定义防重复策略

如果需要实现不同的防重复逻辑,可以创建新的拦截器实现:

@Component
public class TokenBasedInterceptor extends RepeatSubmitInterceptor
{
    @Override
    public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception
    {
        // 基于Token的防重复提交实现
        String token = request.getHeader("X-CSRF-Token");
        if (token != null) {
            // 检查Token是否已使用
            return tokenService.isTokenUsed(token);
        }
        return false;
    }
}

性能优化建议

1. Session存储优化

对于高并发场景,可以考虑使用Redis替代Session存储:

@Component
public class RedisBasedInterceptor extends RepeatSubmitInterceptor
{
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception
    {
        String key = "repeat_submit:" + request.getRequestURI() + ":" + 
                    JSON.toJSONString(request.getParameterMap());
        
        if (redisTemplate.hasKey(key)) {
            return true;
        }
        
        redisTemplate.opsForValue().set(key, "1", annotation.interval(), TimeUnit.MILLISECONDS);
        return false;
    }
}

2. 参数比较优化

对于大型参数,可以使用MD5摘要进行比较:

private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
{
    String nowParams = (String) nowMap.get(REPEAT_PARAMS);
    String preParams = (String) preMap.get(REPEAT_PARAMS);
    
    // 使用MD5摘要比较,避免大字符串比较的性能问题
    String nowMd5 = DigestUtils.md5DigestAsHex(nowParams.getBytes());
    String preMd5 = DigestUtils.md5DigestAsHex(preParams.getBytes());
    
    return nowMd5.equals(preMd5);
}

常见问题解答

Q1: 防重复提交拦截器会影响性能吗?

A: 拦截器本身开销很小,主要性能消耗在于Session操作和参数比较。建议合理设置时间间隔,避免过短的间隔增加不必要的检查。

Q2: 如何处理分布式环境下的重复提交?

A: 在分布式环境中,Session存储可能不适用。建议使用Redis等分布式缓存来存储防重复提交的状态信息。

Q3: 是否可以自定义错误响应格式?

A: 可以重写RepeatSubmitInterceptorpreHandle方法,自定义错误响应的格式和内容。

Q4: 如何测试防重复提交功能?

A: 可以使用Postman或JMeter等工具模拟快速重复请求,验证拦截器是否正常工作。

最佳实践总结

  1. 合理设置时间间隔:根据业务场景设置合适的防重复时间,一般建议2-10秒
  2. 明确错误提示:提供清晰的错误消息,帮助用户理解操作状态
  3. 选择性使用:只在真正需要防重复的业务方法上使用注解
  4. 性能监控:在高并发场景下监控拦截器的性能表现
  5. 分布式适配:在微服务架构中使用分布式存储方案

RuoYi的防重复提交拦截器提供了一个灵活、可扩展的解决方案,通过合理的配置和使用,可以有效防止重复提交问题,提升系统的稳定性和用户体验。

扩展思考

未来改进方向

  1. 支持多种防重复策略:如Token机制、时间戳签名等
  2. 提供可视化配置:通过管理界面配置防重复规则
  3. 集成限流功能:结合限流组件提供更全面的防护
  4. 支持异步请求:优化对Ajax请求的防重复处理

通过深入理解和合理运用RuoYi的防重复提交机制,开发者可以构建更加健壮和用户友好的Web应用程序。

【免费下载链接】RuoYi 🎉 基于SpringBoot的权限管理系统 易读易懂、界面简洁美观。 核心技术采用Spring、MyBatis、Shiro没有任何其它重度依赖。直接运行即可用 【免费下载链接】RuoYi 项目地址: https://gitcode.com/yangzongzhuan/RuoYi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值