由防止表单重复提交引发的一系列问题--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响应内容,让用户知道自己重复提交了。
很简单不举例了;

猜你喜欢

转载自www.cnblogs.com/QGuo/p/10234386.html