由防止表單重復提交引發的一系列問題--servletRequest的復制、body值的獲取


@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 {}
}
HttpServletRequestReplacedFilter
 
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 }
HttpServletRequestWrapper

 

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 }
@SameUrlData

 

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 }
SameUrlDataInterceptor

 

使用的時候,只要在接口上,添加注解即可
例如:
@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響應內容,讓用戶知道自己重復提交了。
很簡單不舉例了;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM