若依源码解析:防止表单重复提交@RepeatSubmit、RepeatableFilter、RepeatedlyRequestWrapper和RepeatSubmitInterceptor
文章目录
摘要
若依(Ruoyi)是一款基于Spring Boot和MyBatis的开源后台管理系统,它提供了一系列的拦截器(Interceptor)用于处理请求。其中,RepeatSubmitInterceptor(重复提交拦截器)是若依系统中的一个关键拦截器,用于防止用户重复提交表单请求。
在Web应用程序中,用户可能会重复提交表单,例如在点击提交按钮后多次点击或者网络延迟造成用户误以为提交未成功而再次提交。这可能导致一些问题,例如重复的数据插入或重复的业务逻辑处理。
RepeatSubmitInterceptor 的主要作用是在用户提交表单请求时,对请求进行拦截和处理,防止重复提交。判断该url是否有RepeatSubmit注解,如果有的话,就里面取到了:【参数,url,用户】然后和RepeatSubmit里的过期时间一起放到了redis。
下次再来的时候会去redis里面去查询,如果已经过期没有,那么通过,然后再重新放入redis.如果还有,那么就不通过。
但这里有个问题,如果参数是从body里面去取来的,那么流会只读一次就再读不到了。于是在此之前使用了RepeatableFilter做了关于这个的封装的字节数组Requestwrapper。
配置拦截器:WebMvcConfigurer
WebMvcConfigurer配置RepeatSubmitInterceptor :
在继承自WebMvcConfigurer的ResourcesConfig会加入RepeatSubmitInterceptor ,RepeatSubmitInterceptor 最主要的方法:this.isRepeatSubmit(request, annotation)是由继承自ResourcesConfig 的SameUrlDataInterceptor 实现的。
@Configurationpublic class ResourcesConfig implements WebMvcConfigurer{ @Autowired private RepeatSubmitInterceptor repeatSubmitInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns(" public int interval() default 5000; public String message() default "不允许重复提交,请稍候再试";}
拦截器具体实现:RepeatSubmitInterceptor和SameUrlDataInterceptor
preHandle:在请求处理之前进行拦截处理。
它会从请求中提取出重复提交所需的标识,并进行重复提交的检查。如果检查到重复提交,可以返回错误信息或者采取其他处理方式。
@Componentpublic abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter{ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); //只有标注了@RepeatSubmit 注解才需要防止表单重复提交,其他的请求直接返回 true。 RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); if (annotation != null) { if (this.isRepeatSubmit(request, annotation)) { AjaxResult ajaxResult = AjaxResult.error(annotation.message()); ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult)); return false; } } return true; } else { return super.preHandle(request, response, handler); } } public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);}
此处的核心是:
//只有标注了@RepeatSubmit 注解才需要防止表单重复提交,其他的请求直接返回 true。RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
验证是否重复提交由子类实现具体的防重复提交的规则
RepeatSubmitInterceptor 最主要的方法:this.isRepeatSubmit(request, annotation)是由继承自ResourcesConfig 的SameUrlDataInterceptor 实现的。
判断该url是否有RepeatSubmit注解,如果有的话,就里面取到了:【参数,url,用户】然后和RepeatSubmit里的过期时间一起放到了redis
@Componentpublic class SameUrlDataInterceptor extends RepeatSubmitInterceptor{ public final String REPEAT_PARAMS = "repeatParams"; public final String REPEAT_TIME = "repeatTime"; // 令牌自定义标识 @Value("${token.header}") private String header; @Autowired private RedisCache redisCache; @SuppressWarnings("unchecked") @Override public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) { String nowParams = ""; if (request instanceof RepeatedlyRequestWrapper) { RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request; nowParams = HttpHelper.getBodyString(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; } // 唯一标识(指定key + 消息头) String cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + submitKey; Object sessionObj = redisCache.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, annotation.interval())) { return true; } } } Map<String, Object> cacheMap = new HashMap<String, Object>(); cacheMap.put(url, nowDataMap); redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS); 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, int interval) { long time1 = (Long) nowMap.get(REPEAT_TIME); long time2 = (Long) preMap.get(REPEAT_TIME); if ((time1 - time2) < interval) { return true; } return false; }}
解决参数读取问题:HttpServletRequest和RepeatedlyRequestWrapper
在项目中经常出现多次读取HTTP请求体的情况,这时候可能就会报错,原因是读取HTTP请求体的操作,最终都要调用HttpServletRequest的getInputStream()方法和getReader()方法,而这两个方法总共只能被调用一次,第二次调用就会报错。
RepeatableFilter用RepeatedlyRequestWrapper 包装了HttpServletRequest。将HttpServletRequest的字节流的数据,保存到一个变量中,重写getInputStream()方法和getReader()方法,从变量中读取数据,返回给调用者。
public class RepeatableFilter implements Filter{ @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ServletRequest requestWrapper = null; if (request instanceof HttpServletRequest && StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) { //包装了HttpServletRequest requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response); } if (null == requestWrapper) { chain.doFilter(request, response); } else { chain.doFilter(requestWrapper, response); } } @Override public void destroy() { }}
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper{ private final byte[] body; public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException { super(request); request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); body = HttpHelper.getBodyString(request).getBytes("UTF-8"); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); }// 重写了,核心:final ByteArrayInputStream bais = new ByteArrayInputStream(body); @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream bais = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public int read() throws IOException { return bais.read(); } @Override public int available() throws IOException { return body.length; } @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } }; }}
来源地址:https://blog.csdn.net/qq_27575627/article/details/130731877
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341