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

三、测试

*** 重复提交测试,在有效时间内,拦截重复提交的数据

posted on 2026-08-10 15:35  爱河  阅读(2)  评论(0)    收藏  举报

导航