一、前言
由于网络原因,用户操作有误(连续点击两次以上提交按钮),或者页面卡顿等原因,可能会出现请求重复提交,造成数据库保存多条重复数据。后端实现拦截器防重。
那么如何防止请求重复提交呢?一般有两种解决方案:
第一种:前端处理,在提交完成之后,将按钮禁用。
第二种:后端处理,使用拦截器拦截。
交给前端解决,判断多长时间内不能再次点击按钮,或者点击之后禁用按钮,当然,聪明的小伙伴能够绕过前端验证,因此推荐后端进行拦截处理。
二、实现思路
使用拦截器防止请求重复提交,本文模仿若依防重给大家分享,利用 AOP 切面在进入方法前拦截,通过 Session 或 Redis 的 key-value 键值对存储,指定 key+url+消息头 来拼成字符串组成 key,使用 请求参数+时间 封装 map 对象赋值 value,当 key 不存在时,则为新的请求;若存在,则对请求参数以及请求的间隔时间进行判断是否重复提交。
2.1、自定义注解防止表单重复提交
java
package com.dian.jiao.interceptor.annotation;
import java.lang.annotation.*;
/**
* 自定义注解防止表单重复提交
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 间隔时间(ms),小于此时间视为重复提交
*/
public int interval() default 5000;
/**
* 提示消息
*/
public String message() default "不允许重复提交,请稍候再试";
}
2.2、构建包装器
java
package com.dian.jiao.interceptor.wrapper;
import com.dian.jiao.util.ServletUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletResponse;
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;
/**
* 构建可重复读取inputStream的request
*/
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
public final String UTF8 = "UTF-8";
private final byte[] body;
public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException {
super(request);
request.setCharacterEncoding(UTF8);
response.setCharacterEncoding(UTF8);
body = ServletUtils.getBodyString(request).getBytes(UTF8);
}
@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) {
}
};
}
}
2.3、防止重复提交拦截器
java
package com.dian.jiao.interceptor;
import com.dian.jiao.interceptor.annotation.RepeatSubmit;
import com.dian.jiao.interceptor.wrapper.RepeatedlyRequestWrapper;
import com.dian.jiao.pojo.User;
import com.dian.jiao.util.CommonUtils;
import com.dian.jiao.util.ServletUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* 防止重复提交拦截器
*/
public class RepeatSubmitInterceptor implements HandlerInterceptor {
@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, annotation)) {
// 将弹框字符串渲染到客户端
ServletUtils.alert(response, annotation.message());
return false;
}
}
}
return true;
}
/**
* 验证是否重复提交,实现具体的防重复提交的规则
*/
@SuppressWarnings("unchecked")
private boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) {
HttpSession session = request.getSession();
User user = (User) session.getAttribute("loginUser");
String nowParams = "";
if (request instanceof RepeatedlyRequestWrapper) {
RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
nowParams = ServletUtils.getBodyString(repeatedlyRequest);
}
// body参数为空,获取Parameter的数据
if (nowParams == null || "".equals(nowParams.trim()) {
nowParams = CommonUtils.toJSONString(request.getParameterMap());
}
Map<String, Object> nowDataMap = new HashMap<>(2);
nowDataMap.put(CommonUtils.REPEAT_PARAMS, nowParams);
nowDataMap.put(CommonUtils.REPEAT_TIME, System.currentTimeMillis());
// 请求地址(作为存放cache的key值)
String url = request.getRequestURI();
// 用户ID
String submitKey = user == null ? ServletUtils.getIpAddr(request) : String.valueOf(user.getId());
// 唯一标识(指定key + url + 消息头)
String cacheRepeatKey = CommonUtils.REPEAT_SUBMIT_KEY + url + submitKey;
Object sessionObj = session.getAttribute(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 (CommonUtils.compareParams(nowDataMap, preDataMap) && CommonUtils.compareTime(nowDataMap, preDataMap, annotation.interval())) {
return true;
}
}
}
Map<String, Object> cacheMap = new HashMap<>(1);
cacheMap.put(url, nowDataMap);
session.setAttribute(cacheRepeatKey, cacheMap);
return false;
}
}
2.4、客户端工具类
java
package com.dian.jiao.util;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* 客户端工具类
*/
public class ServletUtils {
/**
* 获取body请求参数
* @param request 请求对象{@link ServletRequest}
* @return String
*/
public static String getBodyString(ServletRequest request) {
StringBuilder sb = new StringBuilder();
BufferedReader reader = null;
try (InputStream inputStream = request.getInputStream()) {
reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString();
}
/**
* 将弹框字符串渲染到客户端
*
* @param response 渲染对象
* @param msg 待渲染的弹框字符串
*/
public static void alert(HttpServletResponse response, String msg) {
try {
response.reset();
response.setHeader("Content-type", "text/html;charset=UTF-8");
response.setCharacterEncoding("utf-8");
PrintWriter out = response.getWriter();
out.print("<script>");
out.print("alert(\"" + msg + "\");");
out.print("history.back();");
out.print("</script>");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 获取客户端IP
*
* @param request 请求对象
* @return IP地址
*/
public static String getIpAddr(HttpServletRequest request) {
if (request == null) {
return "unknown";
}
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);
}
/**
* 从多级反向代理中获得第一个非unknown IP地址
*
* @param ip 获得的IP地址
* @return 第一个非unknown IP地址
*/
public static String getMultistageReverseProxyIp(String ip) {
// 多级反向代理检测
if (ip != null && ip.indexOf(",") > 0) {
final String[] ips = ip.trim().split(",");
for (String subIp : ips) {
if (false == isUnknown(subIp)) {
ip = subIp;
break;
}
}
}
return StringUtils.substring(ip, 0, 255);
}
/**
* 检测给定字符串是否为未知,多用于检测HTTP请求相关
*
* @param checkString 被检测的字符串
* @return 是否未知
*/
public static boolean isUnknown(String checkString) {
return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString);
}
}
2.5、公共工具类
java
package com.dian.jiao.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
public class CommonUtils {
public static final String REPEAT_PARAMS = "repeatParams";
public static final String REPEAT_TIME = "repeatTime";
public static final String REPEAT_SUBMIT_KEY = "repeat_submit:";
/**
* 判断参数是否相同
*/
public static 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);
}
/**
* 判断两次间隔时间
*/
public static boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval) {
long time1 = (Long) nowMap.get(REPEAT_TIME);
long time2 = (Long) preMap.get(REPEAT_TIME);
if ((time1 - time2) < interval) {
return true;
}
return false;
}
public static String toJSONString(Object object) {
if (object != null) {
try {
return new ObjectMapper().writeValueAsString(object);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
2.6、配置springmvc-servlet.xml,添加拦截器
xml
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<!-- 配置不拦截的请求 -->
<mvc:exclude-mapping path="/login"/>
<mvc:exclude-mapping path="/getCode"/>
<bean class="com.dian.jiao.interceptor.RepeatSubmitInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
三、使用教程
在接口方法上添加 @RepeatSubmit
注解即可,注解参数说明:
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
interval | int | 5000 | 间隔时间(ms),小于此时间视为重复提交 |
message | String | 不允许重复提交,请稍后再试 | 提示消息 |
示例1:采用默认参数
java
@RepeatSubmit
public AjaxResult addSave()
{
return AjaxResult.success();
}
示例2:指定防重复时间和错误消息
java
@RepeatSubmit(interval = 3000, message = "您已经报名,不能重复报名")
public AjaxResult addSave()
{
return AjaxResult.success();
}