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;
}
}