blog/src/programming/java/防止表单和参数重复提交.md
MangMang 14187ec3c7 feat: 更新Linux配置指南与Java面试题文档
- 新增Linux常用配置指南,涵盖VNC、分辨率、语言本地化及时间同步配置
- 重构Java面试题文档,新增选择题、填空题、简答题、编程题等多种题型
- 补充微服务架构相关论述题与分布式缓存场景题解决方案
2025-05-22 22:38:16 +08:00

23 KiB
Raw Blame History

date category tag
2025-05-07
JAVA
表单
重复提交
防重

防止表单和参数重复提交案例

防止表单和参数重复提交

1. 编写注解

/**
 * 防止表单重复提交
 *
 * @author 氓氓编程
 * @Date: 2021-06-08-16:35
 * @Inherited @interface 自定义注解时自动继承了java.lang.annotation.Annotation接口由编译程序自动完成其他细节
 * @Target 用于描述注解的使用范围(作用于方法上)
 * @Retention 被描述的注解在什么范围内有效 (在运行时有效,即运行时保留)
 */
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {

}

2. 自定义拦截器

/**
 * @author 茫茫编程
 * @Date: 2021-06-08-16:29
 */
@Slf4j
@Component
public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter {
    /**
     * @param request  是指经过spring封装的请求对象, 包含请求地址, 头, 参数, body(流)等信息.
     * @param response 是指经过spring封装的响应对象, 包含输入流, 响应body类型等信息.
     * @param handler  是指controller的@Controller注解下的"完整"方法名, 是包含exception等字段信息的.
     * @return  是否放行
     * @throws Exception 异常
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //instanceof是Java中的二元运算符左边是对象右边是类当对象是右边类或子类所创建对象时返回true否则返回false。
        if (handler instanceof HandlerMethod) {
            log.info("进来了");
            //把handler强转为HandlerMethod
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            //获取当前请求的方法
            Method method = handlerMethod.getMethod();
            //获取当前去请求方法上是否有注解@RepeatSubmit
            RepeatSubmit repeatSubmit = method.getAnnotation(RepeatSubmit.class);
            //如果方法有@RepeatSubmit注解进入if
            if (repeatSubmit != null) {
                //判断是否是重复提交,重复提交进入
                if (isRepeatSubmit(request, response)) {
                    //返回消息实体类
                    R message = R.error().message("不允许重复提交");
                    //把消息响应给客户端
                    ServletUtils.renderString(response, JSONUtil.toJsonStr(message));
                    //拦截
                    return false;
                }
            }
            //注解和不重复直接放行
            return true;
        } else {
            //如果handler不是HandlerMethod或子类放行
            return super.preHandle(request, response, handler);
        }
    }

    /**
     * 弗雷调用该方法会使用子类的实现
     * 验证是否重复提交由子类实现具体的防重复提交的规则
     *
     * @param request 请求
     * @return 是否是重复提交
     */
    public abstract boolean isRepeatSubmit(HttpServletRequest request,HttpServletResponse response);
}

3. 自定义拦截器子类

/**
 * 判断请求url和数据是否和上一次相同
 * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
 *
 * @author 氓氓编程
 * @Date: 2021-06-08-17:27
 */
@Slf4j
@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor {
    /**
     * 重复参数
     */
    public final String REPEAT_PARAMS = "repeatParams";

    /**
     * 重复时间
     */
    public final String REPEAT_TIME = "repeatTime";

    /**
     * 间隔时间单位秒
     * 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据
     */
    private int intervalTime = 120;

    public SameUrlDataInterceptor(JwtUtil jwtUtil, RedisCache redisCache) {
        this.jwtUtil = jwtUtil;
        this.redisCache = redisCache;
    }

    public void setIntervalTime(int intervalTime) {
        this.intervalTime = intervalTime;
    }

    private final JwtUtil jwtUtil;

    /**
     * 注入redis
     */
    private final RedisCache redisCache;

    /**
     * 重写父类判断是否重复的抽象方法
     *
     * @param request 请求
     * @return true=重复提交  false=未重复
     */
    @SneakyThrows
    @SuppressWarnings("unchecked")
    @Override
    public boolean isRepeatSubmit(HttpServletRequest request, HttpServletResponse response)  {
        //1.声明当前参数
        String nowParams = null;
        //判断请求是否为空
        if (request != null) {
            //把request 转为可重复获取流的RepeatedlyRequestWrapper
            RepeatedlyRequestWrapper repeatedlyRequest = new RepeatedlyRequestWrapper(request, response);
            //获取body参数
            nowParams = RepeatedlyRequestWrapper.getBodyString(repeatedlyRequest);
        }
        //如果请求体body参数为空获取Parameter的参数
        if (StringUtils.isEmpty(nowParams)) {
            assert request != null;
            nowParams = JSONUtil.toJsonStr(request.getParameterMap());
            log.info("body=={}", nowParams);
        }
        //把数据存储起来
        Map<String, Object> nowMap = new HashMap<>(80);
        nowMap.put(REPEAT_PARAMS, nowParams);
        nowMap.put(REPEAT_TIME, System.currentTimeMillis());
        log.info("nowMap=={}", nowMap);

        // 请求地址作为存放cache的key值
        String url = request.getRequestURI();

        //唯一标识-获取请求头的token值
        String submitKey = request.getHeader(jwtUtil.getHeader());
        //如果token为空,使用请求地址作为key
        if (StringUtils.isEmpty(submitKey)) {
            submitKey = url;
        }
        //唯一标识指定key+消息头)
        String cacheRepeatKey = "repeat_submit:" + submitKey;
        log.info("repeat_submit=={}", cacheRepeatKey);

        //缓存中获取上次请求数据
        Object cacheObject = redisCache.getCacheObject(cacheRepeatKey);
        log.info("cacheObject=={}", cacheObject);
        //如果缓存中没有数据,则存放
        if (cacheObject == null) {
            Map<String, Object> cacheMap = new HashMap<>(109);
            cacheMap.put(url, nowMap);
            redisCache.setCacheObject(cacheRepeatKey, cacheMap, intervalTime, TimeUnit.SECONDS);
        } else {
            //强转为map
            Map<String, Object> preDataMap = (Map<String, Object>) cacheObject;
            //判断该map是否有url作为的键
            if (preDataMap.containsKey(url)) {
                //根据map中的键url 获取对应的参数
                Map<String, Object> preMap = (Map<String, Object>) preDataMap.get(url);
                return compareParams(nowMap, preMap) && compareTime(nowMap, preMap);
            }
        }

        return false;
    }
    
    /**
     * 比较两次请求参数是否相同
     *
     * @param nowMap 现在的数据
     * @param preMap 之前的数据
     * @return true=相同  false=不相同
     */
    private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) {
        String now = (String) nowMap.get(REPEAT_PARAMS);
        String pre = (String) preMap.get(REPEAT_PARAMS);
        return now.equals(pre);
    }

    /**
     * 比较两次请求时间间隔
     *
     * @param nowMap 现在的数据
     * @param preMap 之前的数据
     * @return true=相同  false=不相同
     */
    private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap) {
        long now = (Long) nowMap.get(REPEAT_TIME);
        long pre = (Long) preMap.get(REPEAT_TIME);
        //如果两次间隔时间小于10秒
        return (now - pre) < this.intervalTime * 1000L;
    }
} 

4. 配置拦截器到web中

/**
 * @author 氓氓编程
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    private final RepeatSubmitInterceptor repeatSubmitInterceptor;
	//构造方法注入
    public CorsConfig(RepeatSubmitInterceptor repeatSubmitInterceptor) {
        this.repeatSubmitInterceptor = repeatSubmitInterceptor;
    }
	
    
     /**
     * 解决跨域
     * @return  CorsFilter
     */
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration configuration = new CorsConfiguration();
        // 设置访问源地址(允许那些地址访问服务器)
        configuration.addAllowedOrigin("*");
        // 设置访问源请求方法(方法)
        configuration.addAllowedMethod("*");
        // 设置访问源请求头(头部信息)
        configuration.addAllowedHeader("*");
        // 跨域需要暴露的请求头(因为跨域访问默认不能获取全部头部信息)
        configuration.addExposedHeader("token");
        // 注册配置
        source.registerCorsConfiguration("/**", configuration);
        return new CorsFilter(source);
    }

	/**
     * 添加拦截器
     * @param registry registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(repeatSubmitInterceptor);
    }
}

5. redis

5.1 配置

**
 * @author 氓氓编程
 * 
 */
@Configuration
public class RedisCacheConfig extends CachingConfigurerSupport {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //key序列化
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //value序列化
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

以下是需要用到的工具类

5.2 redis 工具类

/**
 * Redis 工具类
 *
 * @author 氓氓编程
 * @Date: 2021-06-08-17:35
 */
@Component
public class RedisCache {

    private final RedisTemplate<Object, Object> redisTemplate;


    public RedisCache(RedisTemplate<Object, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }


    /**
     * 缓存基本对象Integer、String、实体类等
     *
     * @param key   键
     * @param value 值
     */
    public void setCacheObject(final String key, final Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 带有效时间缓存基本对象Integer、String、实体类等
     *
     * @param key      键
     * @param value    值
     * @param timeout  有效时间
     * @param timeUnit 有效时间单位
     */
    public void setCacheObject(final String key, final Object value, final Integer timeout, final TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 给某个键设置有效时间
     *
     * @param key     需要设置有效时间的键
     * @param timeout 设置的时间  默认是秒
     * @return true=设置成功  false=设置失败
     */
    public boolean expire(final String key, final long timeout) {
        return expire(key, timeout, TimeUnit.SECONDS);
    }


    /**
     * 给某个键设置有效时间
     *
     * @param key      需要设置有效时间的键
     * @param timeout  设置的时间
     * @param timeUnit 有效时间单位
     * @return true=设置成功  false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit timeUnit) {
        Boolean isSuccess = redisTemplate.expire(key, timeout, timeUnit);
        if (isSuccess != null) {
            return isSuccess;
        }
        return false;
    }

    /**
     * 根据键获取某个缓存的值
     *
     * @param key 键
     * @return Object
     */
    public Object getCacheObject(final String key) {
        return redisTemplate.opsForValue().get(key);
    }


    /**
     * 根据建删除缓存
     *
     * @param key 键
     * @return true=成功 false=失败
     */
    public boolean deleteObject(final String key) {
        Boolean isDelete = redisTemplate.delete(key);
        return isDelete != null && isDelete;
    }

    /**
     * 根据键批量删除
     *
     * @param collection 装有键的集合
     * @return 删除成功的数量
     */
    public long deleteObject(final Collection<Object> collection) {
        Long count = redisTemplate.delete(collection);
        return count == null ? 0 : count;
    }

    /**
     * 缓存List集合数据
     *
     * @param key      键
     * @param dataList 待缓存的List数据
     * @return 缓存成功的数量
     */
    public long setCacheList(final String key, final List<Object> dataList) {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获取List缓存数据
     *
     * @param key 键
     * @return 缓存键值对应的数据
     */
    public List<Object> getCacheList(final String key) {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key     键
     * @param dataSet 待缓存的Set数据
     * @return 缓存数据的对象
     */
    public BoundSetOperations<Object, Object> setCacheSet(final String key, final Set<Object> dataSet) {
        BoundSetOperations<Object, Object> setOperations = redisTemplate.boundSetOps(key);
        Iterator<Object> it = dataSet.iterator();
        if (it.hasNext()) {
            setOperations.add(it.next());
        }
        return setOperations;
    }


    /**
     * 获取Set缓存数据
     *
     * @param key 键
     * @return 缓存键值对应的数据
     */
    public Set<Object> getCacheSet(final String key) {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * @param key     键
     * @param dataMap 缓存的map数据
     */
    public void setCacheMap(final String key, final Map<String, Object> dataMap) {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }


    /**
     * 获得缓存的Map
     *
     * @param key 键
     * @return 获得缓存的数据
     */
    public Map<Object, Object> getCacheMap(final String key) {
        return redisTemplate.opsForHash().entries(key);
    }


    /**
     * 往Hash中存入数据
     *
     * @param key   Redis键
     * @param hKey  Hash键
     * @param value 值
     */
    public void setCacheMapValue(final String key, final String hKey, final Object value) {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key  Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public Object getCacheMapValue(final String key, final String hKey) {
        return redisTemplate.opsForHash().get(key, hKey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key   Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public List<Object> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<Object> keys(final String pattern) {
        return redisTemplate.keys(pattern);
    }
}

6. Servlet参数发送的工具类

/**
 * 客户端工具类
 *
 * @author 氓氓编程
 * @Date: 2021-06-08-16:50
 */
public class ServletUtils {
    /**
     * 获取String参数
     */
    public static String getParameter(String name) {
        return getRequest().getParameter(name);
    }

    /**
     * 获取String参数,如墨没有设置一个默认值
     */
    public static String getParameter(String name, String defaultValue) {
        return Convert.toStr(getRequest().getParameter(name), defaultValue);
    }

    /**
     * 获取session
     */
    public static HttpSession getSession() {
        return getRequest().getSession();
    }

    /**
     * 获取Integer参数
     */
    public static Integer getParameterToInt(String name) {
        return Convert.toInt(getRequest().getParameter(name));
    }

    /**
     * 获取response
     */
    public static HttpServletResponse getResponse() {
        return getRequestAttributes().getResponse();
    }

    /**
     * 获取request
     */
    public static HttpServletRequest getRequest() {
        return getRequestAttributes().getRequest();
    }

    /**
     * 获取到当前的HttpServletRequest
     *
     * @return ServletRequestAttributes
     */
    public static ServletRequestAttributes getRequestAttributes() {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        return (ServletRequestAttributes) attributes;
    }

    /**
     * @param response 当前请求的响应
     * @param string   传输的文字
     */
    public static void renderString(HttpServletResponse response, String string) {
        try {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().print(string);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
} 

7. 可重复读取流的RepeatedlyRequestWrapper

/**
 * 构建可重复读取inputStream的请求request
 *
 * @author 氓氓编程
 * @Date: 2021-06-09-8:47
 */
@Slf4j
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {

    /**
     * 存放请求体中的数据
     */
    private final byte[] body;

    public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws UnsupportedEncodingException {
        super(request);
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        this.body = getBodyString(request).getBytes(StandardCharsets.UTF_8);
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream(){
        //1.创建一个字节数组流存放请求体
        final ByteArrayInputStream bodyInputStream = new ByteArrayInputStream(body);
        //2.返回获取的body中的数据流
        return new ServletInputStream() {
            @Override
            public int read(){
                return bodyInputStream.read();
            }

            @Override
            public int available(){
                return body.length;
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }
        };
    }


    /**
     * 获取请求体
     *
     * @param request 请求
     * @return 字符串
     */
    public static String getBodyString(ServletRequest request) {
        //1.创建一个StringBuilder
        StringBuilder sb = new StringBuilder();
        //2.声明一个读缓存的流
        BufferedReader reader = null;
        //3.获取请求中的流
        try (InputStream inputStream = request.getInputStream()) {
            //把请求中的流读取出来给reader
            reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
            //声明line存放每一行的数据
            String line;
            //一行一行的读取数据并赋值给lineline不为空
            while ((line = reader.readLine()) != null) {
                //追加写入
                sb.append(line);
            }
        } catch (IOException e) {
            log.warn("获取请求体中数据出现问题");
        } finally {
            //如果reader不为空关闭流
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    log.error(ExceptionUtil.getMessage(e));
                }
            }
        }
        return sb.toString();
    }
}

8. 让可重复读的流生效

/**
 * 使用重写后的RepeatedlyRequestWrapper
 * <p>
 * Repeatable 过滤器
 */
public class RepeatableFilter implements Filter {
    /**
     * startsWithIgnoreCase 判断开始部分是否与二参数相同。不区分大小写
     *
     * @param request  请求
     * @param response 响应
     * @param chain    放行
     * @throws IOException      io异常
     * @throws ServletException 异常
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //声明一个ServletRequest
        ServletRequest requestWrapper = null;
        //判断request是HttpServletRequest或子类并且request.getContentType()开头包含application/json
        if (request instanceof HttpServletRequest && StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
            //创建一个可重复获取流的RepeatedlyRequestWrapper赋值给ServletRequest
            requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);
        }
        //为空,直接放行
        if (null == requestWrapper) {
            chain.doFilter(request, response);
        } else {
            //赋值完毕可重复读流继续向下传
            chain.doFilter(requestWrapper, response);
        }
    }
}
 	@SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    public FilterRegistrationBean someFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new RepeatableFilter());
        registration.addUrlPatterns("/*");
        registration.setName("repeatableFilter");
        registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
        return registration;
    }