一步一步实现若依框架--2.3防止重复提交 repeat_submit
原理:
常见的场景端页面多次点击提交按钮,通常是前端通过点击一次后使按钮disable进行处理,后端同样也需要进行限制。若依使用了注解+拦截器的方式,这里其实也可以用AOP。在缓存中(若依的缓存就是使用redis)记录每个客户端的请求方法和参数,在redis中设置超时时间。如果在超时时间内进行了第二次请求且参数都一致,拦截器进行拦截抛出异常不进行真正的处理。思路其实和限流类似,只是这里多了对请求参数的处理,并且没有采用AOP而是用了拦截器去实现。
1)添加过滤器,处理请求参数不能多次读的问题。
自定义了一个HttpServletRequest的包装器,通过在变量中保存流中读取的内容,解决了不能多次读入请求参数的问题。然后通过添加过滤器在dofilter中替换了这个增强功能后的request。包装器代码:
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper { private final byte[] body; public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException { super(request); request.setCharacterEncoding(Constants.UTF8); response.setCharacterEncoding(Constants.UTF8); // 把reques的参数读入到byte数组中,就可以多次使用 body= HttpHelper.getBodyString(request).getBytes(Constants.UTF8); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } @Override public ServletInputStream getInputStream() throws IOException { ByteArrayInputStream bis = new ByteArrayInputStream(body); 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 bis.read(); } }; } }
把这个包装器通过过滤器加入到请求处理中去。
public class RepeatableFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { ServletRequest requestWrapper = null; /** * 参数如果是json类型,需要HttpServletRequest包装器进行加强,多次读取参数 */ if(request instanceof HttpServletRequest && request.getContentType().startsWith(MediaType.APPLICATION_JSON_VALUE)){ requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request,response); } if(requestWrapper == null){ filterChain.doFilter(request,response); }else{ filterChain.doFilter(requestWrapper, response); } } }
最后要记得把这个过滤器注册到spring环境里。
@Component public class FilterConfig { @Bean public FilterRegistrationBean repeatableFilterRegistration(){ FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new RepeatableFilter()); registration.addUrlPatterns("/*"); registration.setName("repeatableFilter"); registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE); return registration; } }
springboot通过拦截器获取参数有两种方式,一种通过request.getParameter获取Get方式传递的参数,另外一种是通过request.getInputStream或reques.getReader获取通过POST/PUT/DELETE/PATCH传递的参数。@PathVariable注解是REST风格url获取参数的方式,只能用在GET请求类型,通过getParameter获取参数;@RequestParam注解支持GET和POST/PUT/DELETE/PATCH方式,Get方式通过getParameter获取参数和post方式通过getInputStream或getReader获取参数;@RequestBody注解支持POST/PUT/DELETE/PATCH,可以通过getInputStream和getReader获取参数。通过getInputStream或getReader在拦截器中获取会导致控制器拿到的参数为空,这是因为流读取一次之后流的标志位已经发生了变化,无法多次读取参数,所以通过装饰者模式,增强了HttpServletRequest,把流中的数据读取后暂存在byte[]中,可以实现多次读取的需求。
2)添加拦截器
拦截到自定义注解进行处理 。通过AbstractRepeatSubmitInterceptor拦截请求,进行重复提交的判断和处理。
3)测试
controller中建立添加注解的测试方法:
@PostMapping("/repeatTest") @RepeatSubmit(interval = 100000, message = "重复提交提示!!!") public String repeatTest(@RequestBody String json) { return json; }
代码地址: