@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響應內容,讓用戶知道自己重復提交了。
很簡單不舉例了;