基于注解和拦截器防止表单重复提交

  |   0 评论   |   0 浏览

基于注解和拦截器防止表单重复提交

1.定义防止重复提交标识注解

/**
 * 定义一个需要做重复提交校验标识的注解
 * 该注解用于修饰控制层方法
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RepeatSubmit {
    /**
     * 两个请求之间的间隔时间(时间的单位在redis获取interval值时进行设置)
     * @return
     */
    int interval() default 5000;

    /**
     * 重复提交时候的提示文本
     * @return
     */
    String message() default "不允许重复提交,请稍后再试";
}

2.重写HttpServletRequestWrapper

这里主要解决通过request.getInputStream重复获取请求数据的问题,这里通过自定义的HttpServletRequestWrapper 备份一下流的数据,自定义HttpServletRequestWrapper 调用父类request.getInputStream()读取全部数据出来保存在一个byte数组内,当再次获取流数据的时候,自定义的HttpServletRequestWrapper 就会用byte数组重新生成一个新的流。备份的流数据仍然保留在byte数组中。

public class RepeatableReadRequestWrapper extends HttpServletRequestWrapper {
    private final byte[] bytes;

    public RepeatableReadRequestWrapper(HttpServletRequest request, HttpServletResponse response) throws IOException {
        super(request);
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        bytes = request.getReader().readLine().getBytes();
    }

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

    @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

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

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            @Override
            public int read() throws IOException {
                return bais.read();
            }

            @Override
            public int available() throws IOException {
                return bytes.length;
            }
        };
    }
}

3.定义过滤器

public class RepeatableRequestFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        if (StringUtils.startsWithIgnoreCase(request.getContentType(), "application/json")) {
            RepeatableReadRequestWrapper requestWrapper = new RepeatableReadRequestWrapper(request, (HttpServletResponse) servletResponse);
            filterChain.doFilter(requestWrapper,servletResponse);
            return;
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }

}

4.封装一个Redis缓存工具类

@Component
public class RedisCache {
    @Autowired
    RedisTemplate redisTemplate;

    public <T> void setCacheObject(final String key, final T value, Integer timeout, final TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    public <T> T getCacheObject(final String key) {
        ValueOperations<String,T> valueOperations = redisTemplate.opsForValue();
        return valueOperations.get(key);
    }
}

5.定义重复提交校验拦截器

@Component
public class RepeatSubmitInterceptor implements HandlerInterceptor {

    public static final String REPEAT_PARAMS = "repeat_params";
    public static final String REPEAT_TIME = "repeat_time";
    public static final String REPEAT_SUBMIT_KEY = "repeat_submit_key";
    public static final String HEADER = "Authorization";

    @Autowired
    RedisCache redisCache;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断是否是映射controller层的方法直接通过
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            //获取Method对象
            Method method = handlerMethod.getMethod();
            //获取@RepeatSubmit 注解
            RepeatSubmit repeatSubmit = method.getAnnotation(RepeatSubmit.class);
            //判断是否为空,如果为空则直接放行,如果不为空则进行重复提交校验
            if (repeatSubmit != null) {
                //校验是否重复提交,如果没有则放行,如果是重复提交则返回状态码:500 message:"重复提交时的文本描述"
                if (isRepeatSubmit(request, repeatSubmit)) {
                    Map<String, Object> map = new HashMap<>();
                    //设置状态码
                    map.put("status", 500);
                    //设置描述信息
                    map.put("message", repeatSubmit.message());
                    //设置响应的contentType和编码格式
                    response.setContentType("application/json;charset=utf-8");
                    //new ObjectMapper().writeValueAsString(map)将map对象转换成json字符串
                    response.getWriter().write(new ObjectMapper().writeValueAsString(map));
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * 判断是否重复提交,返回 true 表示是重复提交
     *
     * @param request
     * @param repeatSubmit
     * @return
     */
    private boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit repeatSubmit) {
        //请求参数字符串
        String nowParams = "";
        if (request instanceof RepeatableReadRequestWrapper) {
            try {
                //获取请求体
                nowParams = ((RepeatableReadRequestWrapper) request).getReader().readLine();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //否则说明请求参数是 key-value 格式的
        if (StringUtils.isEmpty(nowParams)) {
            try {
                //获取所有请求参数并转换为json字符串
                nowParams = new ObjectMapper().writeValueAsString(request.getParameterMap());
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        Map<String, Object> nowDataMap = new HashMap<>();
        //key: repeat_params value: nowParams
        nowDataMap.put(REPEAT_PARAMS, nowParams);
        //key: repeat_time value: 时间戳
        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
        // /工程路径/controller层路径
        String requestURI = request.getRequestURI();
        //request.getHeader(“Authorization”) Bearer xxxx
        String header = request.getHeader(HEADER);
        //cacheKey = repeat_submit_key + /工程路径/controller层路径 / xxxx
        String cacheKey = REPEAT_SUBMIT_KEY + requestURI + header.replace("Bearer ", "");
        //从缓存中获取cacheKey的值
        Object cacheObject = redisCache.getCacheObject(cacheKey);
        if (cacheObject != null) {
            Map<String, Object> map = (Map<String, Object>) cacheObject;
            //校验参数、间隔时间是否满足重复提交的校验条件,均满足则返回true,否则返回false
            if (compareParams(map, nowDataMap) && compareTime(map, nowDataMap, repeatSubmit.interval())) {
                return true;
            }
        }
        //从缓存中没有获取到cacheKey的值,设置到redis中
        /**
         * @param1: cacheKey = repeat_submit_key + /工程路径/controller层路径 / xxxx
         * @parma2: 请求数据Map对象:包含请求参数、请求时间戳
         * @parma3: 两次请求的间隔时间,也是redis key的过期时间
         * @parma4: 过期时间的单位MILLISECONDS 毫秒
         */
        redisCache.setCacheObject(cacheKey, nowDataMap, repeatSubmit.interval(), TimeUnit.MILLISECONDS);
        return false;
    }

    /**
     * 对应本次请求和上一次请求的相隔时间是否超过我们设置的间隔时间interval
     * 如果不超过则满足重复提交的校验条件返回true
     * 如果超过则不满足,返回false
     * @param map
     * @param nowDataMap
     * @param interval
     * @return
     */
    private boolean compareTime(Map<String, Object> map, Map<String, Object> nowDataMap, int interval) {
        // 上一次请求的时间
        Long time1 = (Long) map.get(REPEAT_TIME);
        // 本次请求的时间
        Long time2 = (Long) nowDataMap.get(REPEAT_TIME);
        //对比时间差,如果小于我们设置的两个请求之间的间隔时间,则说明满足重复提交的条件返回true
        if ((time2 - time1) < interval) {
            return true;
        }
        //不满足重复提交的间隔时间则返回false
        return false;
    }

    /**
     * 比较本次请求和上一次请求的数据是否一样
     * @param map
     * @param nowDataMap
     * @return
     */
    private boolean compareParams(Map<String, Object> map, Map<String, Object> nowDataMap) {
        String nowParams = (String) nowDataMap.get(REPEAT_PARAMS);
        String dataParams = (String) map.get(REPEAT_PARAMS);
        return nowParams.equals(dataParams);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

6.定义一个Dto

@Data
public class UserDto {

    private String name;

    private Integer age;

    private String nickName;

    private String skill;
}

7.Controller测试类

@RestController
public class HelloController {

    @PostMapping("/hello")
    @RepeatSubmit(interval = 10000)
    public String hello(@RequestBody String json) {
        return json;
    }

    @RepeatSubmit
    @Authorization
    @PostMapping("/test")
    public String authorization(@RequestBody UserDto userDto) {
        System.out.println(userDto);
        return "succes";
    }
}

测试结果

image-20220629171143915


标题:基于注解和拦截器防止表单重复提交
作者:llp
地址:https://llinp.cn/articles/2022/06/29/1656515146327.html