@Time:2019年1月4日 16:19:19
@Author:QGuo
背景:最开始打算写个防止表单重复提交的拦截器;网上见到一种不错的方式,比较合适前后端分离,校验在后台实现;
我在此基础上,将key,value。Objects.hashCode()了下
因为request的body 可能太大,过长;
但不保证存在不同的object生成的哈希值却相同,但是我们目的只是为了防止重复提交而已,不同对象生成哈希值相同的机率很小。
==========================代码==============================
1、HttpServletRequestReplacedFilter 过滤器.
目的:post请求时,复制request;注意代码中的注释部分;
package com.kdgz.service; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * @author QGuo * @date 2019/1/3 15:04 */ public class HttpServletRequestReplacedFilter implements Filter { @Override public void destroy() {} @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ServletRequest requestWrapper = null; if (request instanceof HttpServletRequest) { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String contentType = request.getContentType(); if (contentType != null && contentType.contains("application/x-www-form-urlencoded")) { //如果是application/x-www-form-urlencoded, 参数值在request body中以 a=1&b=2&c=3...形式存在, //若直接构造BodyReaderHttpServletRequestWrapper,在将流读取并存到copy字节数组里之后, //httpRequest.getParameterMap()将返回空值! //若运行一下 httpRequest.getParameterMap(), body中的流将为空! 所以两者是互斥的! request.getParameterMap(); } if ("POST".equals(httpServletRequest.getMethod().toUpperCase())) { requestWrapper = new BodyHttpServletRequestWrapper((HttpServletRequest) request); } } if (requestWrapper == null) { chain.doFilter(request, response); } else { chain.doFilter((HttpServletRequest)requestWrapper, response); } } @Override public void init(FilterConfig arg0) throws ServletException {} }
2、
HttpServletRequestWrapper --复制ServletRequest
目的在于:使servletRequest可以重复获取inputStream、reader;
1 package com.kdgz.service; 2 3 import javax.servlet.ReadListener; 4 import javax.servlet.ServletInputStream; 5 import javax.servlet.http.HttpServletRequest; 6 import javax.servlet.http.HttpServletRequestWrapper; 7 import java.io.BufferedReader; 8 import java.io.ByteArrayInputStream; 9 import java.io.IOException; 10 import java.io.InputStreamReader; 11 import java.nio.charset.Charset; 12 import java.util.Enumeration; 13 import java.util.Map; 14 15 /** 16 * @author QGuo 17 * @date 2019/1/3 15:05 18 */ 19 public class BodyHttpServletRequestWrapper extends HttpServletRequestWrapper { 20 21 private byte[] body; 22 23 public byte[] getBody() { return body; } 24 25 public void setBody(byte[] body) { this.body = body; } 26 27 public BodyHttpServletRequestWrapper(HttpServletRequest request) throws IOException { 28 super(request); 29 body = this.getBodyString(request).getBytes(Charset.forName("UTF-8")); 30 } 31 32 @Override 33 public BufferedReader getReader() throws IOException { 34 return new BufferedReader(new InputStreamReader(getInputStream(),"UTF-8")); 35 } 36 37 @Override 38 public ServletInputStream getInputStream() throws IOException { 39 40 final ByteArrayInputStream bais = new ByteArrayInputStream(this.body); 41 42 return new ServletInputStream() { 43 @Override 44 public boolean isFinished() { return false; } 45 46 @Override 47 public boolean isReady() { return false; } 48 49 @Override 50 public void setReadListener(ReadListener readListener) {} 51 52 @Override 53 public int read() throws IOException { return bais.read(); } 54 }; 55 } 56 57 @Override 58 public String getHeader(String name) { return super.getHeader(name); } 59 60 @Override 61 public Enumeration<String> getHeaderNames() { return super.getHeaderNames(); } 62 63 @Override 64 public Enumeration<String> getHeaders(String name) { return super.getHeaders(name); } 65 66 @Override 67 public Map<String, String[]> getParameterMap() { return super.getParameterMap(); } 68 69 public String getBodyString(ServletRequest request) { 70 StringBuilder sb = new StringBuilder(); 71 InputStream inputStream = null; 72 BufferedReader reader = null; 73 try { 74 inputStream = request.getInputStream(); 75 reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8"))); 76 String line = ""; 77 while ((line = reader.readLine()) != null) { 78 sb.append(line); 79 } 80 } catch (IOException e) { 81 e.printStackTrace(); 82 } finally { 83 if (inputStream != null) { 84 try { 85 inputStream.close(); 86 } catch (IOException e) { 87 e.printStackTrace(); 88 } 89 } 90 if (reader != null) { 91 try { 92 reader.close(); 93 } catch (IOException e) { 94 e.printStackTrace(); 95 } 96 } 97 } 98 return sb.toString(); 99 } 100 }
3、web.xml 中添加过滤器
1 <filter> 2 <filter-name>httpServletRequestFilter</filter-name> 3 <filter-class>com.kdgz.service.HttpServletRequestReplacedFilter</filter-class> 4 </filter> 5 <filter-mapping> 6 <filter-name>httpServletRequestFilter</filter-name> 7 <url-pattern>/*</url-pattern> 8 </filter-mapping>
4、添加自定义注解
1 package com.kdgz.annotation; 2 3 import java.lang.annotation.ElementType; 4 import java.lang.annotation.Retention; 5 import java.lang.annotation.RetentionPolicy; 6 import java.lang.annotation.Target; 7 8 /** 9 * @author QGuo 10 * @date 2018/12/24 13:58 11 * 一个用户 相同url 同时提交 相同数据 验证 12 */ 13 @Target(ElementType.METHOD) 14 @Retention(RetentionPolicy.RUNTIME) 15 public @interface SameUrlData { 16 }
5、添加拦截器
1 package com.kdgz.service; 2 3 import com.alibaba.fastjson.JSON; 4 import com.kdgz.annotation.SameUrlData; 5 import org.apache.commons.lang3.StringUtils; 6 import org.springframework.data.redis.core.StringRedisTemplate; 7 import org.springframework.web.method.HandlerMethod; 8 import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 9 10 import javax.annotation.Resource; 11 import javax.servlet.http.HttpServletRequest; 12 import javax.servlet.http.HttpServletResponse; 13 import java.io.IOException; 14 import java.lang.reflect.Method; 15 import java.util.HashMap; 16 import java.util.Map; 17 import java.util.Objects; 18 import java.util.concurrent.TimeUnit; 19 20 /** 21 * 一个用户 相同url 同时提交 相同数据 验证 22 * 主要通过 session中保存到的url 和 请求参数。如果和上次相同,则是重复提交表单 23 * 24 * @author QGuo 25 * @date 2018/12/24 14:02 26 */ 27 public class SameUrlDataInterceptor extends HandlerInterceptorAdapter { 28 @Resource 29 StringRedisTemplate stringRedisTemplate; 30 31 @Override 32 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 33 if (handler instanceof HandlerMethod) { 34 HandlerMethod handlerMethod = (HandlerMethod) handler; 35 Method method = handlerMethod.getMethod(); 36 SameUrlData annotation = method.getAnnotation(SameUrlData.class); 37 if (annotation != null) { 38 if (repeatDataValidator(request)) {//如果重复相同数据 39 //在此可添加response响应内容,提醒用户重复提交了 40 return false; 41 } else 42 return true; 43 } 44 return true; 45 } else { 46 return super.preHandle(request, response, handler); 47 } 48 } 49 50 /** 51 * 验证同一个url数据是否相同提交 ,相同返回true 52 * 53 * @param httpServletRequest 54 * @return 55 */ 56 public boolean repeatDataValidator(HttpServletRequest httpServletRequest) throws IOException { 57 Map<String, String[]> parameterMap = new HashMap(httpServletRequest.getParameterMap()); 58 //删除参数中的v;(v参数为随机生成的字符串,目的是为了每次访问都是最新值,既然要防止重复提交,需要剔除此参数) 59 if (parameterMap.containsKey("v")) 60 parameterMap.remove("v"); 61 //每一位登录者都有唯一一个token认证 62 String tokens = ""; 63 if (parameterMap.get("token").length > 0) 64 tokens = parameterMap.get("token")[0]; 65 String method = httpServletRequest.getMethod().toUpperCase();//请求类型,GET、POST 66 String params; 67 if (StringUtils.equals(method, "POST")) {//post请求时 68 BodyHttpServletRequestWrapper requestWrapper = new BodyHttpServletRequestWrapper((HttpServletRequest) httpServletRequest); 69 byte[] bytes = requestWrapper.getBody(); 70 if (bytes.length != 0) { 71 params = JSON.toJSONString(new String(bytes, "UTF-8").trim()); 72 } else {//若body被清空,则说明参数全部被填充到Parameter集合中了 73 /** 74 * 当满足一下条件时,就会被填充到parameter集合中 75 * 1:是一个http/https请求 76 * 2:请求方法是post 77 * 3:请求类型(content-Type)是application/x-www-form-urlencoded 78 * 4: Servlet调用了getParameter系列方法 79 */ 80 Map<String, String[]> map = new HashMap(requestWrapper.getParameterMap()); 81 // 去除 v 参数 82 if (map.containsKey("v")) 83 map.remove("v"); 84 params = JSON.toJSONString(map); 85 } 86 } else { 87 params = JSON.toJSONString(parameterMap); 88 } 89 90 String url = String.valueOf(Objects.hashCode(httpServletRequest.getRequestURI() + tokens)); 91 Map<String, String> map = new HashMap<String, String>(); 92 map.put(url, params); 93 //防止参数过多,string过大;现将储存为 hash编码; 94 String nowUrlParams = String.valueOf(Objects.hashCode(map)); 95 String preUrlParams = stringRedisTemplate.opsForValue().get(url); 96 if (preUrlParams == null) {//如果上一个数据为null,表示还没有访问页面 97 //设置过期时间为3分钟 98 stringRedisTemplate.opsForValue().set(url, nowUrlParams, 3, TimeUnit.MINUTES); 99 return false; 100 } else if (preUrlParams.equals(nowUrlParams)) {//否则,已经访问过页面 101 //如果上次url+数据和本次url+数据相同,则表示重复添加数据 102 return true; 103 } else {//如果上次 url+数据 和本次url加数据不同,则不是重复提交,更新 104 stringRedisTemplate.opsForValue().set(url, nowUrlParams, 3, TimeUnit.MINUTES); 105 return false; 106 } 107 } 108 }
使用的时候,只要在接口上,添加注解即可
例如:
@RequestMapping(value = "v1.0/monGraphSave")
@SameUrlData
public AjaxMessage monGraphSave(@RequestBody MonGraphFB monGraphFB){}
====================代码结束===================
整理至此,主要有以下注意点;
①、得考虑post请求参数获取的特殊性
②、request.getInputStream() 只能获取一次,要想可以多次读取,得继承HttpServletRequestWrapper,读出来--放回去
③、过滤器的目的是可以直接读取request里面的body
④、request参数body可能很大,可以取hash值。
⑤、key、value的存储,需要设置过期时间;
心得:
其实我觉得防止表单重复提交这个功能,作用不是特别大;因为只要随便加一个参数,就可以把需要的参数重复添加进系统中;
只能做到,防止用户误操作,点击了多次这种情况;(一般前端也会做处理的,但万一前端抽风自动发起了多次请求呢);
只能说一定程度上 更加完善吧
改进:
可以在SameUrlDataInterceptor拦截器中,添加response响应内容,让用户知道自己重复提交了。
很简单不举例了;