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<String, Object> nowDataMap = new HashMap<String, Object>();
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<String, Object> sessionMap = (Map<String, Object>) sessionObj;
if (sessionMap.containsKey(url))
{
Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap))
{
return true;
}
}
}
Map<String, Object> sessionMap = new HashMap<String, Object>();
sessionMap.put(url, nowDataMap);
session.setAttribute(SESSION_REPEAT_KEY, sessionMap);
return false;
}
/**
* 判断参数是否相同
*/
private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
{
String nowParams = (String) nowMap.get(REPEAT_PARAMS);
String preParams = (String) preMap.get(REPEAT_PARAMS);
return nowParams.equals(preParams);
}
/**
* 判断两次间隔时间
*/
private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> 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 {
}
}
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="${adminPath}/**" />
<bean class="com.jeeplus.modules.sys.interceptor.RepeatSubmitInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
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<String, Object> nowDataMap = new HashMap<String, Object>();
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<String, Object> sessionMap = (Map<String, Object>) sessionObj;
if (sessionMap.containsKey(url))
{
Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap))
{
return true;
}
}
}
Map<String, Object> sessionMap = new HashMap<String, Object>();
sessionMap.put(url, nowDataMap);
session.setAttribute(SESSION_REPEAT_KEY, sessionMap);
return false;
}
/**
* 判断参数是否相同
*/
private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
{
String nowParams = (String) nowMap.get(REPEAT_PARAMS);
String preParams = (String) preMap.get(REPEAT_PARAMS);
return nowParams.equals(preParams);
}
/**
* 判断两次间隔时间
*/
private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> 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<String, Object> 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;
}
}