前言
为什么要防止重复提交?
在实际Java开发过程中,大部分接口都是要实现CRUD的。
例如:
insert操作:这种情况如果多次请求,可能会在数据库中插入多条相同的数据,产生重复数据。
update操作时,如果有累加计算等其他操作,也可能会导致数据被多次修改,导致数据错误。
以上这两种情况在实际开发中是不允许出现的
主要有一下两种解决方案
1:前端处理
前端解决方案是在提交按钮加loading,只运行点击一次,不运行重复点击提交按钮,前端处理是不安全的,一些聪明的小伙伴能够绕过前端验证。
2:后端处理
具体实现思路在下文。
后端处理实现思路
使用拦截器防止请求重复提交,利用 AOP 切面在进入方法前拦截,通过 Session 或 Redis 的 key-value 键值对存储,指定 key+url或消息头 来拼成字符串组成 key,使用 请求参数+时间 封装 map 对象赋值 value,当 key 不存在时,则为新的请求;若存在,则对请求参数以及请求的间隔时间进行判断是否重复提交
参考若依实现方案
- 自定义注解防止表单重复提交
package com.freight.common.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解防止表单重复提交
*
* @author freight
*
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{
}
- 构建可重复读取inputStream的request
package com.freight.common.filter;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import com.freight.common.utils.http.HttpHelper;
/**
* 构建可重复读取inputStream的request
*
* @author freight
*/
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper
{
private final byte[] body;
public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException
{
super(request);
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
body = HttpHelper.getBodyString(request).getBytes("UTF-8");
}
@Override
public BufferedReader getReader() throws IOException
{
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@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 int available() throws IOException
{
return body.length;
}
@Override
public boolean isFinished()
{
return false;
}
@Override
public boolean isReady()
{
return false;
}
@Override
public void setReadListener(ReadListener readListener)
{
}
};
}
}
- 防止重复提交拦截器
package com.freight.framework.interceptor;
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import com.alibaba.fastjson.JSONObject;
import com.freight.common.annotation.RepeatSubmit;
import com.freight.common.core.domain.AjaxResult;
import com.freight.common.utils.ServletUtils;
/**
* 防止重复提交拦截器
*
* @author freight
*/
@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, JSONObject.toJSONString(ajaxResult));
return false;
}
}
return true;
}
else
{
return super.preHandle(request, response, handler);
}
}
/**
* 验证是否重复提交由子类实现具体的防重复提交的规则
*
* @param request
* @return
* @throws Exception
*/
public abstract boolean isRepeatSubmit(HttpServletRequest request);
}
package com.freight.framework.interceptor.impl;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSONObject;
import com.freight.common.constant.Constants;
import com.freight.common.core.redis.RedisCache;
import com.freight.common.filter.RepeatedlyRequestWrapper;
import com.freight.common.utils.StringUtils;
import com.freight.common.utils.http.HttpHelper;
import com.freight.framework.interceptor.RepeatSubmitInterceptor;
/**
* 判断请求url和数据是否和上一次相同,
* 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
*
* @author freight
*/
@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
{
public final String REPEAT_PARAMS = "repeatParams";
public final String REPEAT_TIME = "repeatTime";
// 令牌自定义标识
@Value("${token.header}")
private String header;
@Autowired
private RedisCache redisCache;
/**
* 间隔时间,单位:秒 默认10秒
*
* 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据
*/
private int intervalTime = 10;
public void setIntervalTime(int intervalTime)
{
this.intervalTime = intervalTime;
}
@SuppressWarnings("unchecked")
@Override
public boolean isRepeatSubmit(HttpServletRequest request)
{
String nowParams = "";
if (request instanceof RepeatedlyRequestWrapper)
{
RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
nowParams = HttpHelper.getBodyString(repeatedlyRequest);
}
// body参数为空,获取Parameter的数据
if (StringUtils.isEmpty(nowParams))
{
nowParams = JSONObject.toJSONString(request.getParameterMap());
}
Map<String, Object> nowDataMap = new HashMap<String, Object>();
nowDataMap.put(REPEAT_PARAMS, nowParams);
nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
// 请求地址(作为存放cache的key值)
String url = request.getRequestURI();
// 唯一值(没有消息头则使用请求地址)
String submitKey = request.getHeader(header);
if (StringUtils.isEmpty(submitKey))
{
submitKey = url;
}
// 唯一标识(指定key + 消息头)
String cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + submitKey;
Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
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> cacheMap = new HashMap<String, Object>();
cacheMap.put(url, nowDataMap);
redisCache.setCacheObject(cacheRepeatKey, cacheMap, intervalTime, TimeUnit.SECONDS);
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;
}
}
- 配置
@Configuration
public class ResourcesConfig implements WebMvcConfigurer {
@Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor;
/**
* 自定义拦截规则
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
}
}
- 测试
/**
* 新增参数配置
*/
@PreAuthorize("@ss.hasPermi('system:config:add')")
@Log(title = "参数管理", businessType = BusinessType.INSERT)
@PostMapping
@RepeatSubmit
public AjaxResult add(@Validated @RequestBody SysConfig config)
{
if (UserConstants.NOT_UNIQUE.equals(configService.checkConfigKeyUnique(config)))
{
return AjaxResult.error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在");
}
config.setCreateBy(getUsername());
return toAjax(configService.insertConfig(config));
}