SpringBoot防止Web重复提交
一、前言
客户端重复请求是Web系统中一类很常见的问题,有时我们会发现,前后端都增加了业务校验,但是数据库中仍然有重复数据,这个时候就要怀疑是不是用户重复点击了提交按钮。一些用户请求在某些情况下是可能重复发送的,如果是查询类操作并无大碍,但其中有些涉及写入操作,一旦重复了,可能会导致很严重的后果。
二、快速开始
2.1、pom依赖引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
2.2、config中创建配合类
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
/**
* RedisTemplate配置
* Title: RedisTemplateConfiguration
* Description:
*/
@Configuration
@Component
public class MyRedisConfig {
/**
*
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
// 设置redis连接
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// 将redisTemplate的序列化方式更改为StringRedisSerializer
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
2.3、自定义切面注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author wno5974
* @create 2023-07-03 12:01
* @Deprecated 防止表单重复提交注解
*/
@Target(ElementType.METHOD) // 注解的作用目标为方法
@Retention(RetentionPolicy.RUNTIME) // 注解的保留期限为运行时
public @interface PreventSubmit {
/**
* 时间(s)
*/
int time() default 5;
}
2.4、自定义切面编程
import com.alibaba.fastjson.JSON;
import com.won.pure.anntion.PreventSubmit;
import com.won.pure.system.general.handle.InputStreamHttpServletRequestWrapper;
import com.won.pure.utils.JwtHelper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;
/**
* @author wno5974
* @create 2023-07-03 12:02
* @Deprecated
*/
@Aspect // 表明这是一个切面类
@Component // 表示这是一个Bean
public class PreventSubmitAccept {
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 定义切入点,即标注了@PreventDuplicateSubmission注解的方法
@Pointcut("@annotation(com.won.pure.anntion.PreventSubmit)")
public void preventDuplicateSubmission() {
}
@Around("preventDuplicateSubmission()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
InputStreamHttpServletRequestWrapper requestWrapper = new InputStreamHttpServletRequestWrapper(request);
String requestURI = request.getRequestURI();
if(!checkUrl(requestURI)){
return joinPoint.proceed();
}else{
String token = requestWrapper.geHeadString(requestWrapper);
String username = JwtHelper.getUserName(token);
String key = "preventSubmit:"+username+":"+requestURI + ":" + JSON.toJSONString(request.getParameterMap());
if (stringRedisTemplate.hasKey(key)) { // 如果Redis中已存在该请求
throw new RuntimeException("请勿重复提交");
}
// 获取注解的参数
PreventSubmit formSubmission = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(PreventSubmit.class);
int time = formSubmission.time();
// 设置请求的key和value,有效期为3秒
stringRedisTemplate.opsForValue().set(key, "1", time, TimeUnit.SECONDS);
return joinPoint.proceed();
}
}
//路由过滤
protected static boolean checkUrl(String url) {
boolean flag = true;
String checkValue = "fastdfs,websocket,login,swagger,v2,file,verificationCode,loginPhone,videoCallServerEndpoint";
String[] split = checkValue.split(",");
for (String str : split) {
if (url.contains(str)) {
flag = false;
break;
} else {
flag = true;
continue;
}
}
return flag;
}
}
2.5、实际使用
@PreventSubmit
@RequestMapping("dict/saveDict")
@ResponseBody
public Result saveDict(@RequestBody SysDict sysDict) {
boolean save = sysDictService.save(sysDict);
if (save) {
return Result.ok(Collections.EMPTY_LIST);
} else {
return Result.fail(Collections.EMPTY_LIST);
}
}
三、测试
*** 重复提交测试,在有效时间内,拦截重复提交的数据
浙公网安备 33010602011771号