本文已参与「新人创作礼」活动,一起开启掘金创作之路。
1. 场景描述
- 用于用户误操作,多次点击表单提交按钮
- 由于网速等原因造成页面卡顿,用户重复刷新提交页面
- 恶用户如利用postman等工具重复恶意提交表单
这些情况都会造成表单的重复提交,造成数据重复,防止表单重复提交,有以下4种方案:
- 通过JavaScript屏蔽提交按钮,但仍然不安全
- 数据库增加唯一键约束 (简单粗暴)
- 利用session防止表单重复提交-拦截器(推荐):服务返回表单页面时,会生成一个token,当表单提交时,token失效;token为空或者失效则表单提交失败(发送token,验证token)
- 使用Spring AOP自定义切入实现
2. AOP自定义切入实现
(1)自定义重复提交注解(noRepeatSubmit)
(2)对于防止重复提交的Controller里的方法加上注解
(3)新增Aspect切入点,为noRepeatSubmit加入切入点
(4)每次提交表单时,aspect都会保存当前key到redis(设置过期时间)
(5)重复提交时aspect会判断当前redis是否有该key,若有则拦截
3. 代码
3.1 pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>no-repeat-submit</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>no-repeat-submit</name>
<description>no-repeat-submit</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--自定义切入点-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--SpringBoot与Redis整合依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.2 自定义注解(noRepeatSubmit)
@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {
/**
* 指定时间内不可重复提交,单位毫秒
* @return
*/
int timeout() default 3000;
}
3.3 RepeatSubmitAspect
@Slf4j
@Aspect
@Component
public class RepeatSubmitAspect {
@Resource
private RedisTemplate<Integer, String> redisTemplate;
@Around("@annotation(noRepeatSubmit)")
public Object around(ProceedingJoinPoint point, NoRepeatSubmit noRepeatSubmit) throws Throwable {
//获取注解
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
//获取类,方法
String className = method.getDeclaringClass().getName();
String methodName = method.getName();
//组装key:用户唯一标识+操作类和方法
String key = "用户token#" + className + "#" + methodName;
int keyHashCode = Math.abs(key.hashCode());
log.info("key:{},keyHashcode:{}", key, keyHashCode);
//获取超时时间
int timeOut = noRepeatSubmit.timeout();
log.info("超时时间{}", timeOut);
// 从缓存给中根据key获取数据
String value = redisTemplate.opsForValue().get(keyHashCode);
if (value != null) {
log.info("重复提交");
//如果value不为空; return "请勿重复提交";
return "重复提交,稍后重试";
} else {
log.info("首次提交");
// value为空,则加入缓存,并设置过期过期时间
redisTemplate.opsForValue().set(keyHashCode, "1", timeOut, TimeUnit.MILLISECONDS);
}
//执行Object
Object object = point.proceed();
return object;
}
}
3.4 TestController接口方法使用注解
@RestController
public class TestController {
/**
* 测试防止重复提交
*
* @return
*/
@NoRepeatSubmit(timeout = 4000)
@RequestMapping(value = "/commit", method = RequestMethod.GET)
public String noRepeatCommit() {
return "ok";
}
}