阻止表单重复提交拦截器,springmvc、springboot及springboot前后端分离系统的处理方式

作者: adm 分类: java 发布时间: 2023-10-11

springmvc版

package com.jeeplus.modules.sys.interceptor;


import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.jeeplus.common.json.AjaxJson;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;


import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;


/**
* 重复提交拦截器
* @author: wyj
* @date: 2020/06/04
*/
public class RepeatSubmitInterceptor implements HandlerInterceptor {
    public final String REPEAT_PARAMS = "repeatParams";


    public final String REPEAT_TIME = "repeatTime";


    public final String SESSION_REPEAT_KEY = "repeatData";
    /**
     * 间隔时间,单位:秒 默认10秒
     *
     * 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据
     */
    private int intervalTime = 10;
    private static final ObjectMapper objectMapper = new ObjectMapper();
    private static final ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter();
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {
        if (handler instanceof HandlerMethod)
        {
            if (this.isRepeatSubmit(httpServletRequest))
            {
                AjaxJson ajaxJson = new AjaxJson();
                ajaxJson.setMsg("不允许重复提交,请稍后再试");
                ajaxJson.setSuccess(false);
                renderString(httpServletResponse, objectWriter.writeValueAsString(ajaxJson));
                return false;
            }
            return true;
        }
        else
        {
            return true;
        }
    }
    public boolean isRepeatSubmit(HttpServletRequest request) throws Exception
    {
        // 本次参数及系统时间
        String nowParams = objectWriter.writeValueAsString(request.getParameterMap());
        Map nowDataMap = new HashMap();
        nowDataMap.put(REPEAT_PARAMS, nowParams);
        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());


        // 请求地址(作为存放session的key值)
        String url = request.getRequestURI();


        HttpSession session = request.getSession();
        Object sessionObj = session.getAttribute(SESSION_REPEAT_KEY);
        if (sessionObj != null)
        {
            Map sessionMap = (Map) sessionObj;
            if (sessionMap.containsKey(url))
            {
                Map preDataMap = (Map) sessionMap.get(url);
                if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap))
                {
                    return true;
                }
            }
        }
        Map sessionMap = new HashMap();
        sessionMap.put(url, nowDataMap);
        session.setAttribute(SESSION_REPEAT_KEY, sessionMap);
        return false;
    }


    /**
     * 判断参数是否相同
     */
    private boolean compareParams(Map nowMap, Map preMap)
    {
        String nowParams = (String) nowMap.get(REPEAT_PARAMS);
        String preParams = (String) preMap.get(REPEAT_PARAMS);
        return nowParams.equals(preParams);
    }
    /**
     * 判断两次间隔时间
     */
    private boolean compareTime(Map nowMap, Map preMap)
    {
        long time1 = (Long) nowMap.get(REPEAT_TIME);
        long time2 = (Long) preMap.get(REPEAT_TIME);
        if ((time1 - time2) < (this.intervalTime * 1000))
        {
            return true;
        }
        return false;
    }


    /**
     * 将字符串渲染到客户端
     *
     * @param response 渲染对象
     * @param string 待渲染的字符串
     * @return null
     */
    public static String renderString(HttpServletResponse response, String string)
    {
        try
        {
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }














    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {


    }


    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {


    }
}






springboot版拦截器

/**
* 防止重复提交拦截器
*
*/
@Component
public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter
{
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
    {
        if (handler instanceof HandlerMethod)
        {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
            if (annotation != null)
            {
                if (this.isRepeatSubmit(request))
                {
                    AjaxResult ajaxResult = AjaxResult.error("不允许重复提交,请稍后再试");
                    ServletUtils.renderString(response, JSON.marshal(ajaxResult));
                    return false;
                }
            }
            return true;
        }
        else
        {
            return super.preHandle(request, response, handler);
        }
    }


    /**
     * 验证是否重复提交由子类实现具体的防重复提交的规则
     *
     * @param request
     * @return
     * @throws Exception
     */
    public abstract boolean isRepeatSubmit(HttpServletRequest request) throws Exception;
}
/**
* 判断请求url和数据是否和上一次相同,
* 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
*
*/
@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
{
    public final String REPEAT_PARAMS = "repeatParams";


    public final String REPEAT_TIME = "repeatTime";


    public final String SESSION_REPEAT_KEY = "repeatData";


    /**
     * 间隔时间,单位:秒 默认10秒
     *
     * 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据
     */
    private int intervalTime = 10;


    public void setIntervalTime(int intervalTime)
    {
        this.intervalTime = intervalTime;
    }


    @SuppressWarnings("unchecked")
    @Override
    public boolean isRepeatSubmit(HttpServletRequest request) throws Exception
    {
        // 本次参数及系统时间
        String nowParams = JSON.marshal(request.getParameterMap());
        Map nowDataMap = new HashMap();
        nowDataMap.put(REPEAT_PARAMS, nowParams);
        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());


        // 请求地址(作为存放session的key值)
        String url = request.getRequestURI();


        HttpSession session = request.getSession();
        Object sessionObj = session.getAttribute(SESSION_REPEAT_KEY);
        if (sessionObj != null)
        {
            Map sessionMap = (Map) sessionObj;
            if (sessionMap.containsKey(url))
            {
                Map preDataMap = (Map) sessionMap.get(url);
                if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap))
                {
                    return true;
                }
            }
        }
        Map sessionMap = new HashMap();
        sessionMap.put(url, nowDataMap);
        session.setAttribute(SESSION_REPEAT_KEY, sessionMap);
        return false;
    }


    /**
     * 判断参数是否相同
     */
    private boolean compareParams(Map nowMap, Map preMap)
    {
        String nowParams = (String) nowMap.get(REPEAT_PARAMS);
        String preParams = (String) preMap.get(REPEAT_PARAMS);
        return nowParams.equals(preParams);
    }


    /**
     * 判断两次间隔时间
     */
    private boolean compareTime(Map nowMap, Map preMap)
    {
        long time1 = (Long) nowMap.get(REPEAT_TIME);
        long time2 = (Long) preMap.get(REPEAT_TIME);
        if ((time1 - time2) < (this.intervalTime * 1000))
        {
            return true;
        }
        return false;
    }
}

springboot前后端分离+redis版
问题背景:
前后端分离项目,不允许重复提交

前端提交参数使用:request payload方法,想要从对应request中获取参数只能读流,并且流只能读一次
因此通过对request对象进行替换方式进行解决,通过过滤器识别post请求,将post请求的request替换为自定义实现的request,后续方法中获取参数从自定义的request中获取
阻止重复提交,通过拦截器实现
在一定时间内禁止重复提交,通过redis的过期时间进行控制

@PostMapping
public AjaxResult add(@RequestBody ModelDifferenceDetail modelDifferenceDetail)
{
    return toAjax(modelDifferenceDetailService.insertModelDifferenceDetail(modelDifferenceDetail));
}

对应处理

package com.ctsi.ssdc.common.handler;


import java.lang.annotation.*;


/**
* @author: wyj
* @date: 2020/11/26
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AllowedRepeat {
}
package com.ctsi.ssdc.common.handler;


import org.apache.commons.lang3.StringUtils;


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.StandardCharsets;


/**
* 替换流
* @author: wyj
* @date: 2020/11/26
*/
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
    private final byte[] body;
    private boolean passFlag;


    public boolean isPassFlag() {
        return passFlag;
    }


    public BodyReaderHttpServletRequestWrapper(HttpServletRequest request)
            throws IOException {
        super(request);
        StringBuilder sb = new StringBuilder();
        String line;
        BufferedReader reader = request.getReader();
        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
        String body = sb.toString();
        passFlag=StringUtils.isNotEmpty(body);
        this.body = body.getBytes(StandardCharsets.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(body);
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return bais.read();
            }


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


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


            @Override
            public void setReadListener(ReadListener readListener) {


            }
        };
    }


}
package com.ctsi.ssdc.common.handler;


import org.springframework.util.DigestUtils;


import java.io.UnsupportedEncodingException;


/**
* @author: wyj
* @date: 2020/11/26
*/
public class MD5Util {
    private static final String slat = "&%h3k5j*fhd*9&d%fh$#@";
    /**
     * 生成md5
     * @param str
     * @return
     */
    public static String getMD5(String str) throws UnsupportedEncodingException {
        String base = str +"/"+slat;
        String md5 = DigestUtils.md5DigestAsHex(base.getBytes("UTF-8"));
        return md5;
    }
}
package com.ctsi.ssdc.common.handler;




import com.alibaba.fastjson.JSON;
import com.ctsi.ssdc.common.base.AjaxResult;
import com.ctsi.ssdc.common.util.ServletUtils;


import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;


import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;


/**
* @author wyj
* @Date 2020-11-25
* @Description
*/
public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            AllowedRepeat annotation = method.getAnnotation(AllowedRepeat.class);
            if(annotation!=null){
                return true;
            }
            if (this.isRepeatSubmit(request)) {
                AjaxResult ajaxResult = AjaxResult.error("不允许重复提交,请稍后再试");
                ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
                return false;
            }
            return true;
        } else {
            return super.preHandle(request, response, handler);
        }


    }




    /**
     * 验证是否重复提交由子类实现具体的防重复提交的规则
     *
     * @param request
     * @return
     * @throws Exception
     */
    public abstract boolean isRepeatSubmit(HttpServletRequest request) throws Exception;
}
package com.ctsi.ssdc.common.handler;


import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Enumeration;


/**
* 处理流不能重复读问题过滤器(替换原来的request)
* @author: wyj
* @date: 2020/11/26
*/
public class ReplaceRequestFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {


    }


    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if(request instanceof HttpServletRequest) {
            HttpServletRequest r = (HttpServletRequest) request;
            String method = r.getMethod();
            String header = r.getHeader("content-type");
            //只允许post提交并且仅处理request payload
            if("post".equalsIgnoreCase(method)&&(header.contains("application/json")||header.contains("multipart/form-data"))){
                BodyReaderHttpServletRequestWrapper nrq = new BodyReaderHttpServletRequestWrapper((HttpServletRequest) request);
                if(nrq.isPassFlag()){
                    requestWrapper=nrq;
                }
            }
        }
        if(requestWrapper == null) {
            chain.doFilter(request, response);
        } else {
            chain.doFilter(requestWrapper, response);
        }
    }


    @Override
    public void destroy() {


    }
}
package com.ctsi.ssdc.common.handler;






import org.springframework.data.redis.core.RedisTemplate;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.concurrent.TimeUnit;


/**
* 判断请求url和数据是否和上一次相同,
* 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
*
* @author sah
*/
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
{
    @Resource
    private RedisTemplate redisTemplate;


    @SuppressWarnings("unchecked")
    @Override
    public boolean isRepeatSubmit(HttpServletRequest request) throws Exception
    {
        // 本次参数及系统时间
        String method = request.getMethod();
        String header = request.getHeader("content-type");
        //只允许post提交并且仅处理request payload
        if(!method.equalsIgnoreCase("post")||!(header.contains("application/json")||header.contains("multipart/form-data"))){
            //只处理post和request payload
            return false;
        }
        String nowParams= getData(request);
        String authorization = request.getHeader("Authorization");
        String url = request.getRequestURI();
        //参数 + url + 授权确保唯一,md5加密减小key的长度,通过redis设置过期
        String p= url+"&"+nowParams+"&"+authorization;
        String key = MD5Util.getMD5(p);
        Object o = redisTemplate.opsForValue().get(key);
        if(o!=null){
            return true;
        }
        //3秒过期
        redisTemplate.opsForValue().set(key, "1", 3, TimeUnit.SECONDS);
        return false;
    }


    private String getData(HttpServletRequest request) throws IOException {
        StringBuilder sb = new StringBuilder();
        String line;
        BufferedReader reader = request.getReader();
        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
        String body = sb.toString();
        return body;
    }




}
package com.ctsi.ssdc.config;


import com.ctsi.ssdc.common.handler.ReplaceRequestFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


/**
* 过滤器配置
* @author: wyj
* @date: 2020/11/26
*/
@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean registFilter() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new ReplaceRequestFilter());
        registration.addUrlPatterns("/*");
        registration.setName("LogCostFilter");
        registration.setOrder(1);
        return registration;
    }
}
package com.ctsi.ssdc.config;


import com.ctsi.ssdc.common.handler.ReplaceRequestFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


/**
* 过滤器配置
* @author: wyj
* @date: 2020/11/26
*/
@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean registFilter() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new ReplaceRequestFilter());
        registration.addUrlPatterns("/*");
        registration.setName("LogCostFilter");
        registration.setOrder(1);
        return registration;
    }
}

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!