SpringBoot拦截器防重复提交实战

springboot防重复提交实现

  • 开发中可能会经常遇到短时间内由于用户的重复点击导致几秒之内重复的请求,可能就是在这几秒之内由于各种问题,比如 网络 ,事务的隔离性等等问题导致了数据的重复等问题,因此在日常开发中必须规
    避这类的重复请求操作,今天就用拦截器处理一下这个问题

自定义注解

java 复制代码
package test;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {

    /**
     * 默认失效时间时间(秒),小于或等于0表示不启用
     */
    long seconds() default 5;

}

拦截器逻辑

java 复制代码
package test;

import com.ruoyi.common.exception.RepeatSubmitException;
import com.sun.org.apache.xpath.internal.operations.Bool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 重复请求的拦截器
 *
 * @Component:注解将当前类注入到IOC容器中
 */
@Component
public class RepeatSubmitInterceptor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            //只拦截@RepeatSubmit注解
            HandlerMethod method = (HandlerMethod) handler;
            //标注在方法上的注解
            RepeatSubmit repeatSubmitByMethod = AnnotationUtils.findAnnotation(method.getMethod(), RepeatSubmit.class);
            //标注在类上的注解
            RepeatSubmit repeatSubmitByCls = AnnotationUtils.findAnnotation(method.getMethod().getDeclaringClass(), RepeatSubmit.class);
            //组合判断条件,根据自己项目实际需求来,这里只用简单的身份标识做拦截示例
            //没有限制重复提交,直接跳过
            if (Objects.isNull(repeatSubmitByMethod) && Objects.isNull(repeatSubmitByCls))
                return true;

            //优先使用方法级注解,其次使用类级注解
            RepeatSubmit repeatSubmit = Objects.nonNull(repeatSubmitByMethod) ? repeatSubmitByMethod : repeatSubmitByCls;

            //验证注解配置的有效性:失效时间必须大于0
            if(repeatSubmit.seconds() <= 0){
                return true;
            }

            //请求url
            String uri = request.getRequestURI() ;

            //构建更精确的Redis Key:结合用户标识防止不同用户间误判
            String userKey = getUserIdentifier(request);
            String redisKey = userKey + ":" + uri;

            //redis中存在返回false,不存在返回true
            Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, "1", Objects.nonNull(repeatSubmitByMethod) ? repeatSubmitByMethod.seconds() : repeatSubmitByCls.seconds(), TimeUnit.SECONDS);
            //如果存在,表示已经请求过了,直接抛出异常,由全局异常进行处理返回指定信息
            if (ifAbsent != null && !ifAbsent) {
                throw new RepeatSubmitException();
            }

            return true;
        }
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }


    /**
     * 获取用户唯一标识
     * 优先级:Token > SessionId > IP地址
     */
    private String getUserIdentifier(HttpServletRequest request) {
        //尝试从请求头获取Token
        String token = request.getHeader("Authorization");
        if (StringUtils.hasText(token)) {
            return token;
        }

        //尝试获取SessionId
        String sessionId = request.getRequestedSessionId();
        if (StringUtils.hasText(sessionId)) {
            return sessionId;
        }

        //使用IP地址作为兜底方案
        String ip = getClientIp(request);
        return StringUtils.hasText(ip) ? ip : "unknown";
    }

    /**
     * 获取客户端真实IP
     */
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
            //多次反向代理后会有多个IP值,第一个为真实IP
            int index = ip.indexOf(',');
            if (index != -1) {
                return ip.substring(0, index);
            }
            return ip;
        }

        ip = request.getHeader("X-Real-IP");
        if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
            return ip;
        }

        ip = request.getHeader("Proxy-Client-IP");
        if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
            return ip;
        }

        ip = request.getHeader("WL-Proxy-Client-IP");
        if (StringUtils.hasText(ip) && !"unknown".equalsIgnoreCase(ip)) {
            return ip;
        }

        return request.getRemoteAddr();
    }
}

配置拦截器

java 复制代码
package test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private RepeatSubmitInterceptor repeatSubmitInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        final String[] commonExclude={"/error","/files/**"};
        registry.addInterceptor(repeatSubmitInterceptor)
                .excludePathPatterns(commonExclude);
    }
}

抛异常代码

java 复制代码
package com.ruoyi.common.exception;

/**
 * 重复提交异常
 * 
 * @author ruoyi
 */
public class RepeatSubmitException extends RuntimeException
{
    private static final long serialVersionUID = 1L;

    /**
     * 错误提示
     */
    private String message;

    /**
     * 空构造方法
     */
    public RepeatSubmitException()
    {
        this("不允许重复提交,请稍后再试");
    }

    public RepeatSubmitException(String message)
    {
        super(message);
        this.message = message;
    }

    @Override
    public String getMessage()
    {
        return message;
    }

    public RepeatSubmitException setMessage(String message)
    {
        this.message = message;
        return this;
    }
}
相关推荐
2401_894915532 小时前
GEO 优化源码全解析:从搜索引擎到 AI 引擎的底层改写逻辑
java·服务器·前端·数据库·人工智能·分布式·搜索引擎
suaizai_3 小时前
Fastjson高危漏洞紧急修复指南
spring boot
Java牛马6 小时前
SpringBoot Starter 依赖相关总结
java·spring·springboot·starter·自动装配·依赖
吴声子夜歌6 小时前
Java面试——Spring Cloud原理及应用(二)
java·spring cloud·面试
SomeB1oody6 小时前
【RustyML入门】6.3. 并行归约
开发语言·后端·机器学习·rust·教程
东小西7 小时前
【SAA实战】第 1 篇:ReactAgent 入门——先撸个"会调工具的助手"跑起来
java·人工智能·spring
Profile排查笔记7 小时前
指纹浏览器哪个好?从 Profile、代理、权限和自动化能力判断是否适合
前端·人工智能·后端·自动化
用户938515635077 小时前
用 AI 结对编程从 0 搭一个"单词后台管理系统":Next.js + Supabase + Drizzle + shadcn/ui 全记录
后端·postgresql·next.js
mqiqe7 小时前
AgentScope Java 2.0 协议集成全景解析:A2A、AG-UI、Agent Protocol 三大开放协议实战指南
java·开发语言·ui
脉动数据行情8 小时前
Java 实现台股 TWSE/TPEx 行情采集(个股 + K 线)
java·开发语言·twse·tpex·台股