aop解决 防重复提交

背景描述

虽然前端控制了按钮不能连续点击,但是在网络信号弱的情况下,仍然会出现第一次点击,请求A网络信号弱,这个时候前端按钮仍然可以点击,然后用户点击第二次。结果两次请求全部成功,数据库生成了两条除了ID以外一模一样的数据。(业务上不允许这种数据出现)

解决方式

采用AOP,对于不能重复提交的接口在后端加上控制。

第一步 自定义注解

/**
 * @Author ztc
 * @Description 防止重复提交自定义注解
 * @Date 2023/3/16 11:43
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatCheck {
}

第二步 写一个HttpServletRequest包装类

原因:对于接口入参有@RequestBody修饰的情况,如果再次获取httpServletRequest中的body参数时,会出现异常,异常描述大意就是已经获取过一次body参数了,不能再获取第二次。因此我们需要将HttpServletRequest包装,通过我们自己写的包装类获取body参数。

/**
 * @Author ztc
 * @Description HttpServletRequest包装类
 * @Date 2023/3/16 12:59
 */
public class MyHttpServletRequestWrapper extends HttpServletRequestWrapper {
    public String body;

    public MyHttpServletRequestWrapper (HttpServletRequest request) throws IOException {
        super(request);
        StringBuffer sBuffer = new StringBuffer();
        BufferedReader bufferedReader = request.getReader();
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            sBuffer.append(line);
        }
        body = sBuffer.toString();
    }

    @Override
    public ServletInputStream getInputStream() {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
        return new ServletInputStream() {
            @Override
            public int read() {
                return byteArrayInputStream.read();
            }

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

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

            @Override
            public void setReadListener(ReadListener listener) {

            }
        };
    }

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

第三步 有了包装类了那我们还需要写一个过滤器,将HttpServletRequest包装

这里要注意,将有MultipartFile(也就是有文件上传的)接口,要过滤掉。因为他们即使包装了也会抛异常。(还是不能获取第二次的那个异常)

/**
 * @Author ztc
 * @Description request过滤器
 * @Date 2023/3/16 13:01
 */
public class RequestFilter implements Filter {
    //配置接口过滤
    private  final Set<String> ALLOWED_PATHS = Collections.unmodifiableSet(new HashSet<>(
            Arrays.asList("/import","/api/upload")));

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if(request instanceof HttpServletRequest) {
            HttpServletRequest servletRequest = (HttpServletRequest) request;
            String requestURI = servletRequest.getRequestURI();
            if (ALLOWED_PATHS.contains(requestURI)){
                chain.doFilter(servletRequest,response);
            }else {
                requestWrapper = new MyHttpServletRequestWrapper(servletRequest);
                //获取请求中的流,将取出来的字符串,再次转换成流,然后把它放入到新request对象中。
                // 在chain.doFiler方法中传递新的request对象
                chain.doFilter(requestWrapper, response);
            }
        }
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

然后加载到配置类中

/**
 * @Author ztc
 * @Description web配置类
 * @Date 2023/3/16 13:03
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Bean
    public FilterRegistrationBean filterRegist() {
        FilterRegistrationBean frBean = new FilterRegistrationBean();
        frBean.setFilter(new RequestFilter());
        frBean.addUrlPatterns("/*");
        return frBean;
    }
}

第四步 最后我们只需要再写一个切面就好了

/**
 * @Author ztc
 * @Description 校验重复注解切面
 * @Date 2023/3/16 11:56
 */
@Aspect
@Component
@Slf4j
public class RepeatChekAspect {
		//这个是自己写的redis的工具类
    @Autowired
    private RedisUtils redisUtils;

    public final String REPEAT_PARAMS = "repeatParams";

    public final String REPEAT_TIME = "repeatTime";
    // 令牌自定义标识
    @Value("${token.header}")
    private String header;


    @Before("@annotation(RepeatCheck的依赖路径)")
    public void before(JoinPoint point) {
        ServletRequestAttributes requestAttributes =
                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        boolean repeatSubmit = isRepeatSubmit(request);
        if (repeatSubmit) {
            throw new BusinessException("10秒内请勿重复提交");
        }
    }

    /**
     * 间隔时间,单位:秒 默认10秒
     * 

* 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据 */ private final long intervalTime = 10L; @SuppressWarnings("unchecked") public boolean isRepeatSubmit(HttpServletRequest request) { String nowParams = ""; if (request instanceof MyHttpServletRequestWrapper) { MyHttpServletRequestWrapper repeatedlyRequest = (MyHttpServletRequestWrapper)request; nowParams = HttpUtils.read(repeatedlyRequest); } // body参数为空,获取Parameter的数据 if (StringUtils.isEmpty(nowParams)) { nowParams = JSONObject.toJSONString(request.getParameterMap()); } Map<String, Object> nowDataMap = new HashMap<String, Object>(); nowDataMap.put(REPEAT_PARAMS, nowParams); nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); // 请求地址(作为存放cache的key值) String url = request.getRequestURI(); // 唯一值(没有消息头则使用请求地址) String submitKey = request.getHeader(header); if (StringUtils.isEmpty(submitKey)) { submitKey = url; } log.info("submitKey={}",submitKey); // 唯一标识(指定key + 消息头) String cacheRepeatKey = RedisCacheEnum.REPEAT_SUBMIT_KEY + submitKey; Object sessionObj = redisUtils.getCacheObject(cacheRepeatKey); if (sessionObj != null) { Map<String, Object> sessionMap = (Map<String, Object>) sessionObj; if (sessionMap.containsKey(url)) { Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url); if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap)) { return true; } } } Map<String, Object> cacheMap = new HashMap<String, Object>(); cacheMap.put(url, nowDataMap); redisUtils.setCacheObject(cacheRepeatKey, cacheMap, intervalTime, TimeUnit.SECONDS); return false; } /** * 判断参数是否相同 */ private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) { String nowParams = (String) nowMap.get(REPEAT_PARAMS); String preParams = (String) preMap.get(REPEAT_PARAMS); return nowParams.equals(preParams); } /** * 判断两次间隔时间 */ private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap) { long time1 = (Long) nowMap.get(REPEAT_TIME); long time2 = (Long) preMap.get(REPEAT_TIME); if ((time1 - time2) < (this.intervalTime * 1000)) { return true; } return false; } }

然后我们只需要在防止重复提交的接口上加@RepeatCheck注解就好了

2023-03-27 发现问题

当接口入参为 form-data格式时,仍然会出现
org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.lang.IllegalStateException: getReader() has already been called for this request。
原因在于 FileItemIteratorImpl 这个类的 init方法中的LimitedInputStream

protected void init(FileUploadBase fileUploadBase, RequestContext pRequestContext) throws FileUploadException, IOException {
        String contentType = this.ctx.getContentType();
        if (null != contentType && contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/")) {
            long requestSize = ((UploadContext)this.ctx).contentLength();
            Object input;
            if (this.sizeMax >= 0L) {
                if (requestSize != -1L && requestSize > this.sizeMax) {
                    throw new SizeLimitExceededException(String.format("the request was rejected because its size (%s) exceeds the configured maximum (%s)", requestSize, this.sizeMax), requestSize, this.sizeMax);
                }
				//------------------------这一句报的错----------------------------
                input = new LimitedInputStream(this.ctx.getInputStream(), this.sizeMax) {
                    protected void raiseError(long pSizeMax, long pCount) throws IOException {
                        FileUploadException ex = new SizeLimitExceededException(String.format("the request was rejected because its size (%s) exceeds the configured maximum (%s)", pCount, pSizeMax), pCount, pSizeMax);
                        throw new FileUploadIOException(ex);
                    }
                };
            } else {
                input = this.ctx.getInputStream();
            }

            String charEncoding = fileUploadBase.getHeaderEncoding();
            if (charEncoding == null) {
                charEncoding = this.ctx.getCharacterEncoding();
            }

            this.multiPartBoundary = fileUploadBase.getBoundary(contentType);
            if (this.multiPartBoundary == null) {
                IOUtils.closeQuietly((Closeable)input);
                throw new FileUploadException("the request was rejected because no multipart boundary was found");
            } else {
                this.progressNotifier = new MultipartStream.ProgressNotifier(fileUploadBase.getProgressListener(), requestSize);

                try {
                    this.multiPartStream = new MultipartStream((InputStream)input, this.multiPartBoundary, this.progressNotifier);
                } catch (IllegalArgumentException var9) {
                    IOUtils.closeQuietly((Closeable)input);
                    throw new InvalidContentTypeException(String.format("The boundary specified in the %s header is too long", "Content-type"), var9);
                }

                this.multiPartStream.setHeaderEncoding(charEncoding);
            }
        } else {
            throw new InvalidContentTypeException(String.format("the request doesn't contain a %s or %s stream, content type header is %s", "multipart/form-data", "multipart/mixed", contentType));
        }
    }

当调用this.ctx.getInputStream()方法时,并不会走到我们自己的包装类MyHttpServletRequestWrapper 中的getInputStream方法,即使我们在过滤器中放行的是我们自己的包装类。
aop解决 防重复提交_第1张图片
我想这也是第三步中 我们配置接口过滤的原因。遗憾的是我想不出解决办法,所以最后我没有再封装所有的请求流,改成了只对需要防重复提交的接口进行封装。
如下:

/**
 * @Author ztc
 * @Description request过滤器
 * @Date 2023/3/16 13:01
 */
public class RequestFilter implements Filter {
    //配置接口过滤  只处理需要防重的接口
    private  final List<String> ALLOWED_PATHS = Arrays.asList("/initiateAudit");

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if(request instanceof HttpServletRequest) {
            HttpServletRequest servletRequest = (HttpServletRequest) request;
            String requestURI = servletRequest.getRequestURI();
            if (contains(requestURI)){
                requestWrapper = new MyHttpServletRequestWrapper(servletRequest);
                //获取请求中的流如何,将取出来的字符串,再次转换成流,然后把它放入到新request对象中。
                // 在chain.doFiler方法中传递新的request对象
                chain.doFilter(requestWrapper, response);
            }else {
                chain.doFilter(servletRequest,response);
            }
        }
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }

    private boolean contains(String requestURI){
        return ALLOWED_PATHS.stream().anyMatch(requestURI::endsWith);
    }
}

希望有大佬能指点一下,怎么解决form-data数据即使包装了仍然会出现getReader() has already been called for this request的问题。

你可能感兴趣的:(java,spring)