防止重复提交自定义注解@NoRepeatSubmit

1,091 阅读2分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

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