由防止表单重复提交引发的一系列问题--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 {}
}

2. HttpServletRequestWrapper --复制ServletRequest

目的在于:使servletRequest可以重复获取inputStream、reader;

package com.kdgz.service;

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.Charset;
import java.util.Enumeration;
import java.util.Map;

/**
 * @author QGuo
 * @date 2019/1/3 15:05
 */
public class BodyHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private byte[] body;

    public byte[] getBody() { return body; }

    public void setBody(byte[] body) { this.body = body; }

    public BodyHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        body = this.getBodyString(request).getBytes(Charset.forName("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(this.body);

        return new ServletInputStream() {
            @Override
            public boolean isFinished() {  return false; }

            @Override
            public boolean isReady() { return false; }

            @Override
            public void setReadListener(ReadListener readListener) {}

            @Override
            public int read() throws IOException { return bais.read(); }
        };
    }

    @Override
    public String getHeader(String name) { return super.getHeader(name); }

    @Override
    public Enumeration<String> getHeaderNames() { return super.getHeaderNames(); }

    @Override
    public Enumeration<String> getHeaders(String name) { return super.getHeaders(name); }

    @Override
    public Map<String, String[]> getParameterMap() { return super.getParameterMap(); }
    
    public String getBodyString(ServletRequest request) {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        BufferedReader reader = null;
        try {
            inputStream = request.getInputStream();
            reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
            String line = "";
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }
}

3、web.xml 中添加过滤器

  <filter>
    <filter-name>httpServletRequestFilter</filter-name>
    <filter-class>com.kdgz.service.HttpServletRequestReplacedFilter</filter-class>
  </filter>
  <filter-mapping>
      <filter-name>httpServletRequestFilter</filter-name>
      <url-pattern>/*</url-pattern>
  </filter-mapping>

4、添加自定义注解

package com.kdgz.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author QGuo
 * @date 2018/12/24 13:58
 * 一个用户 相同url 同时提交 相同数据 验证
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SameUrlData {
}

5、添加拦截器

package com.kdgz.service;

import com.alibaba.fastjson.JSON;
import com.kdgz.annotation.SameUrlData;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 一个用户 相同url 同时提交 相同数据 验证
 * 主要通过 session中保存到的url 和 请求参数。如果和上次相同,则是重复提交表单
 *
 * @author QGuo
 * @date 2018/12/24 14:02
 */
public class SameUrlDataInterceptor extends HandlerInterceptorAdapter {
    @Resource
    StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            SameUrlData annotation = method.getAnnotation(SameUrlData.class);
            if (annotation != null) {
                if (repeatDataValidator(request)) {//如果重复相同数据
                    //在此可添加response响应内容,提醒用户重复提交了
                    return false;
                } else
                    return true;
            }
            return true;
        } else {
            return super.preHandle(request, response, handler);
        }
    }

    /**
     * 验证同一个url数据是否相同提交  ,相同返回true
     *
     * @param httpServletRequest
     * @return
     */
    public boolean repeatDataValidator(HttpServletRequest httpServletRequest) throws IOException {
        Map<String, String[]> parameterMap = new HashMap(httpServletRequest.getParameterMap());
        //删除参数中的v;(v参数为随机生成的字符串,目的是为了每次访问都是最新值,既然要防止重复提交,需要剔除此参数)
        if (parameterMap.containsKey("v"))
            parameterMap.remove("v");
        //每一位登录者都有唯一一个token认证
        String tokens = "";
        if (parameterMap.get("token").length > 0)
            tokens = parameterMap.get("token")[0];
        String method = httpServletRequest.getMethod().toUpperCase();//请求类型,GET、POST
        String params;
        if (StringUtils.equals(method, "POST")) {//post请求时
            BodyHttpServletRequestWrapper requestWrapper = new BodyHttpServletRequestWrapper((HttpServletRequest) httpServletRequest);
            byte[] bytes = requestWrapper.getBody();
            if (bytes.length != 0) {
                params = JSON.toJSONString(new String(bytes, "UTF-8").trim());
            } else {//若body被清空,则说明参数全部被填充到Parameter集合中了
                /**
                 * 当满足一下条件时,就会被填充到parameter集合中
                 * 1:是一个http/https请求
                 * 2:请求方法是post
                 * 3:请求类型(content-Type)是application/x-www-form-urlencoded
                 * 4: Servlet调用了getParameter系列方法
                 */
                Map<String, String[]> map = new HashMap(requestWrapper.getParameterMap());
                // 去除 v 参数
                if (map.containsKey("v"))
                    map.remove("v");
                params = JSON.toJSONString(map);
            }
        } else {
            params = JSON.toJSONString(parameterMap);
        }

        String url = String.valueOf(Objects.hashCode(httpServletRequest.getRequestURI() + tokens));
        Map<String, String> map = new HashMap<String, String>();
        map.put(url, params);
        //防止参数过多,string过大;现将储存为 hash编码;
        String nowUrlParams = String.valueOf(Objects.hashCode(map));
        String preUrlParams = stringRedisTemplate.opsForValue().get(url);
        if (preUrlParams == null) {//如果上一个数据为null,表示还没有访问页面
            //设置过期时间为3分钟
            stringRedisTemplate.opsForValue().set(url, nowUrlParams, 3, TimeUnit.MINUTES);
            return false;
        } else if (preUrlParams.equals(nowUrlParams)) {//否则,已经访问过页面
            //如果上次url+数据和本次url+数据相同,则表示重复添加数据
            return true;
        } else {//如果上次 url+数据 和本次url加数据不同,则不是重复提交,更新
            stringRedisTemplate.opsForValue().set(url, nowUrlParams, 3, TimeUnit.MINUTES);
            return false;
        }
    }
}

使用的时候,只要在接口上,添加注解即可

例如:

@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响应内容,让用户知道自己重复提交了。

很简单不举例;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值