阻止表单重复提交拦截器,springmvc、springboot及springboot前后端分离系统的处理方式
springmvc版
package com.jeeplus.modules.sys.interceptor; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.jeeplus.common.json.AjaxJson; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * 重复提交拦截器 * @author: wyj * @date: 2020/06/04 */ public class RepeatSubmitInterceptor implements HandlerInterceptor { public final String REPEAT_PARAMS = "repeatParams"; public final String REPEAT_TIME = "repeatTime"; public final String SESSION_REPEAT_KEY = "repeatData"; /** * 间隔时间,单位:秒 默认10秒 * * 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据 */ private int intervalTime = 10; private static final ObjectMapper objectMapper = new ObjectMapper(); private static final ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter(); @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception { if (handler instanceof HandlerMethod) { if (this.isRepeatSubmit(httpServletRequest)) { AjaxJson ajaxJson = new AjaxJson(); ajaxJson.setMsg("不允许重复提交,请稍后再试"); ajaxJson.setSuccess(false); renderString(httpServletResponse, objectWriter.writeValueAsString(ajaxJson)); return false; } return true; } else { return true; } } public boolean isRepeatSubmit(HttpServletRequest request) throws Exception { // 本次参数及系统时间 String nowParams = objectWriter.writeValueAsString(request.getParameterMap()); MapnowDataMap = new HashMap (); nowDataMap.put(REPEAT_PARAMS, nowParams); nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); // 请求地址(作为存放session的key值) String url = request.getRequestURI(); HttpSession session = request.getSession(); Object sessionObj = session.getAttribute(SESSION_REPEAT_KEY); if (sessionObj != null) { Map sessionMap = (Map ) sessionObj; if (sessionMap.containsKey(url)) { Map preDataMap = (Map ) sessionMap.get(url); if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap)) { return true; } } } Map sessionMap = new HashMap (); sessionMap.put(url, nowDataMap); session.setAttribute(SESSION_REPEAT_KEY, sessionMap); return false; } /** * 判断参数是否相同 */ private boolean compareParams(Map nowMap, Map preMap) { String nowParams = (String) nowMap.get(REPEAT_PARAMS); String preParams = (String) preMap.get(REPEAT_PARAMS); return nowParams.equals(preParams); } /** * 判断两次间隔时间 */ private boolean compareTime(Map nowMap, Map 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; } /** * 将字符串渲染到客户端 * * @param response 渲染对象 * @param string 待渲染的字符串 * @return null */ public static String renderString(HttpServletResponse response, String string) { try { response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().print(string); } catch (IOException e) { e.printStackTrace(); } return null; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } }
springboot版拦截器
/** * 防止重复提交拦截器 * */ @Component public 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 annotation = method.getAnnotation(RepeatSubmit.class); if (annotation != null) { if (this.isRepeatSubmit(request)) { AjaxResult ajaxResult = AjaxResult.error("不允许重复提交,请稍后再试"); ServletUtils.renderString(response, JSON.marshal(ajaxResult)); return false; } } return true; } else { return super.preHandle(request, response, handler); } } /** * 验证是否重复提交由子类实现具体的防重复提交的规则 * * @param request * @return * @throws Exception */ public abstract boolean isRepeatSubmit(HttpServletRequest request) throws Exception; }
/** * 判断请求url和数据是否和上一次相同, * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。 * */ @Component public class SameUrlDataInterceptor extends RepeatSubmitInterceptor { public final String REPEAT_PARAMS = "repeatParams"; public final String REPEAT_TIME = "repeatTime"; public final String SESSION_REPEAT_KEY = "repeatData"; /** * 间隔时间,单位:秒 默认10秒 * * 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据 */ private int intervalTime = 10; public void setIntervalTime(int intervalTime) { this.intervalTime = intervalTime; } @SuppressWarnings("unchecked") @Override public boolean isRepeatSubmit(HttpServletRequest request) throws Exception { // 本次参数及系统时间 String nowParams = JSON.marshal(request.getParameterMap()); MapnowDataMap = new HashMap (); nowDataMap.put(REPEAT_PARAMS, nowParams); nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); // 请求地址(作为存放session的key值) String url = request.getRequestURI(); HttpSession session = request.getSession(); Object sessionObj = session.getAttribute(SESSION_REPEAT_KEY); if (sessionObj != null) { Map sessionMap = (Map ) sessionObj; if (sessionMap.containsKey(url)) { Map preDataMap = (Map ) sessionMap.get(url); if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap)) { return true; } } } Map sessionMap = new HashMap (); sessionMap.put(url, nowDataMap); session.setAttribute(SESSION_REPEAT_KEY, sessionMap); return false; } /** * 判断参数是否相同 */ private boolean compareParams(Map nowMap, Map preMap) { String nowParams = (String) nowMap.get(REPEAT_PARAMS); String preParams = (String) preMap.get(REPEAT_PARAMS); return nowParams.equals(preParams); } /** * 判断两次间隔时间 */ private boolean compareTime(Map nowMap, Map 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; } }
springboot前后端分离+redis版
问题背景:
前后端分离项目,不允许重复提交
前端提交参数使用:request payload方法,想要从对应request中获取参数只能读流,并且流只能读一次
因此通过对request对象进行替换方式进行解决,通过过滤器识别post请求,将post请求的request替换为自定义实现的request,后续方法中获取参数从自定义的request中获取
阻止重复提交,通过拦截器实现
在一定时间内禁止重复提交,通过redis的过期时间进行控制
@PostMapping public AjaxResult add(@RequestBody ModelDifferenceDetail modelDifferenceDetail) { return toAjax(modelDifferenceDetailService.insertModelDifferenceDetail(modelDifferenceDetail)); }
对应处理
package com.ctsi.ssdc.common.handler; import java.lang.annotation.*; /** * @author: wyj * @date: 2020/11/26 */ @Inherited @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface AllowedRepeat { }
package com.ctsi.ssdc.common.handler; import org.apache.commons.lang3.StringUtils; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; /** * 替换流 * @author: wyj * @date: 2020/11/26 */ public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper { private final byte[] body; private boolean passFlag; public boolean isPassFlag() { return passFlag; } public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException { super(request); StringBuilder sb = new StringBuilder(); String line; BufferedReader reader = request.getReader(); while ((line = reader.readLine()) != null) { sb.append(line); } String body = sb.toString(); passFlag=StringUtils.isNotEmpty(body); this.body = body.getBytes(StandardCharsets.UTF_8); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream(),"UTF-8")); } @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 boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } }; } }
package com.ctsi.ssdc.common.handler; import org.springframework.util.DigestUtils; import java.io.UnsupportedEncodingException; /** * @author: wyj * @date: 2020/11/26 */ public class MD5Util { private static final String slat = "&%h3k5j*fhd*9&d%fh$#@"; /** * 生成md5 * @param str * @return */ public static String getMD5(String str) throws UnsupportedEncodingException { String base = str +"/"+slat; String md5 = DigestUtils.md5DigestAsHex(base.getBytes("UTF-8")); return md5; } }
package com.ctsi.ssdc.common.handler; import com.alibaba.fastjson.JSON; import com.ctsi.ssdc.common.base.AjaxResult; import com.ctsi.ssdc.common.util.ServletUtils; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; /** * @author wyj * @Date 2020-11-25 * @Description */ public 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(); AllowedRepeat annotation = method.getAnnotation(AllowedRepeat.class); if(annotation!=null){ return true; } if (this.isRepeatSubmit(request)) { AjaxResult ajaxResult = AjaxResult.error("不允许重复提交,请稍后再试"); ServletUtils.renderString(response, JSON.toJSONString(ajaxResult)); return false; } return true; } else { return super.preHandle(request, response, handler); } } /** * 验证是否重复提交由子类实现具体的防重复提交的规则 * * @param request * @return * @throws Exception */ public abstract boolean isRepeatSubmit(HttpServletRequest request) throws Exception; }
package com.ctsi.ssdc.common.handler; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.Enumeration; /** * 处理流不能重复读问题过滤器(替换原来的request) * @author: wyj * @date: 2020/11/26 */ public class ReplaceRequestFilter 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) { HttpServletRequest r = (HttpServletRequest) request; String method = r.getMethod(); String header = r.getHeader("content-type"); //只允许post提交并且仅处理request payload if("post".equalsIgnoreCase(method)&&(header.contains("application/json")||header.contains("multipart/form-data"))){ BodyReaderHttpServletRequestWrapper nrq = new BodyReaderHttpServletRequestWrapper((HttpServletRequest) request); if(nrq.isPassFlag()){ requestWrapper=nrq; } } } if(requestWrapper == null) { chain.doFilter(request, response); } else { chain.doFilter(requestWrapper, response); } } @Override public void destroy() { } }
package com.ctsi.ssdc.common.handler; import org.springframework.data.redis.core.RedisTemplate; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.io.BufferedReader; import java.io.IOException; import java.util.concurrent.TimeUnit; /** * 判断请求url和数据是否和上一次相同, * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。 * * @author sah */ public class SameUrlDataInterceptor extends RepeatSubmitInterceptor { @Resource private RedisTemplateredisTemplate; @SuppressWarnings("unchecked") @Override public boolean isRepeatSubmit(HttpServletRequest request) throws Exception { // 本次参数及系统时间 String method = request.getMethod(); String header = request.getHeader("content-type"); //只允许post提交并且仅处理request payload if(!method.equalsIgnoreCase("post")||!(header.contains("application/json")||header.contains("multipart/form-data"))){ //只处理post和request payload return false; } String nowParams= getData(request); String authorization = request.getHeader("Authorization"); String url = request.getRequestURI(); //参数 + url + 授权确保唯一,md5加密减小key的长度,通过redis设置过期 String p= url+"&"+nowParams+"&"+authorization; String key = MD5Util.getMD5(p); Object o = redisTemplate.opsForValue().get(key); if(o!=null){ return true; } //3秒过期 redisTemplate.opsForValue().set(key, "1", 3, TimeUnit.SECONDS); return false; } private String getData(HttpServletRequest request) throws IOException { StringBuilder sb = new StringBuilder(); String line; BufferedReader reader = request.getReader(); while ((line = reader.readLine()) != null) { sb.append(line); } String body = sb.toString(); return body; } }
package com.ctsi.ssdc.config; import com.ctsi.ssdc.common.handler.ReplaceRequestFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 过滤器配置 * @author: wyj * @date: 2020/11/26 */ @Configuration public class FilterConfig { @Bean public FilterRegistrationBean registFilter() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new ReplaceRequestFilter()); registration.addUrlPatterns("/*"); registration.setName("LogCostFilter"); registration.setOrder(1); return registration; } }
package com.ctsi.ssdc.config; import com.ctsi.ssdc.common.handler.ReplaceRequestFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 过滤器配置 * @author: wyj * @date: 2020/11/26 */ @Configuration public class FilterConfig { @Bean public FilterRegistrationBean registFilter() { FilterRegistrationBean registration = new FilterRegistrationBean(); registration.setFilter(new ReplaceRequestFilter()); registration.addUrlPatterns("/*"); registration.setName("LogCostFilter"); registration.setOrder(1); return registration; } }