@TOC
大家可以看到效果非常好,一秒钟一千次请求都是拦截住了,即使报错也是客户端Socket关闭
各位大哥好,首先介绍一下什么是表单重复提交. (原谅我中英文句点不分,是因为开发的时候把中文的符号也切换成
了英文的)一个场景就是小弟我在实习的时候做的第一个前后端分离的RESTFul接口,说白了也就是普通的增删改
查,项目验收的时候,牛逼的部门经理小方发现了一个bug,删除一个实体(实体是轮播图,属性有id,图片url地
址,文字,创建日期等)的时候报错了;
然后我就顺着情况准备去复现一下,然后发现实在复现不了,百般无奈请教了我的师傅华佗.听说他老人家是华为出来
的...卧槽说多了,师傅带我一阵排查,从本地debug到具体执行的sql日志再到前端分析,最后一天的时间师傅说
实在找不出来你就乱点点,我一听也就放弃了,重点来了!!!!就在我随便点击的时候,一直疯狂点击添加按钮,终
于发现了bug所在,添加按钮点击很快的时候,往数据库插入了两条一模一样的数据,如下图
大家会发现这两条记录在我疯狂手速的点击下竟然是除了主键ID外是一模一样的,然后最骚的Bug来了,点击第一张
轮播图删除键的时候,会根据附件图片的ID地址去另一张附件表删除,用的是delete from xxx
where imageId =xxx,这个时候是不会报错的然后你刷新列表,就会发现第二张图没了,为什么呢,因为它已经
不存在附件表里了,就是这么骚的操作.扯了一大圈,现在附上会出现表单重复提交的原因;
1.提交完表单以后,不做其他操作,直接刷新页面,表单会提交多次。
2.在提交表单时,如果网速较差,可能会导致点击提交按钮多次,这种情况也会导致表单重复提交。
3.单提交成功以后,直接点击浏览器上回退按钮,不刷新页面,然后点击提交按钮再次提交表单。
怎么解决表单重复提交?首先可以前端js在提交按钮点击的同时设置按钮不可点击,当然这个可以被
绕过,咱们还是重点说一下后端怎么做的吧.
场景:添加用户,添加框打开的同时,请求后端一个接口,返回token(只要是加密唯一串即可),存入redis,前端拿到token后,在提交按钮点击的同时,重写请求头,把token加入的header中,然后后端拿到token,去和redis当中的值做对比,如果二者相等,则redis.delete(token);如果redis当中没有值,就证明已经被delete了,即表单已经提交一次,不允许重复提交;如果二者不相等,很明显,伪造token,直接返回
package com.qdu.niit.util.token;
import com.qdu.niit.config.redisconfig.RedisKey;
import com.qdu.niit.core.Result;
import com.qdu.niit.core.ResultGenerator;
import com.qdu.niit.util.StringUtil;
import com.qdu.niit.util.redis.RedisUtil;
import com.xiaoleilu.hutool.util.RandomUtil;
import com.xiaoleilu.hutool.util.StrUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* @author [email protected]
* @version 0.5
* @date Created in 2020-04-13 17:17
* @description 描述
* @modified By
*/
@Component
public class TokenUtil {
@Autowired
private RedisUtil redisUtil;
public String createToken() {
String token = RandomUtil.randomUUID();
String tokenKey = RedisKey.getToken(token);
redisUtil.set(tokenKey, token, 60 * 10L);
return token;
}
public Result checkToken(HttpServletRequest request) throws Exception {
String token = request.getHeader("Authorization");
// header中不存在token
if (StrUtil.isBlank(token)) {
token = request.getParameter("Authorization");
// parameter中也不存在token
if (StrUtil.isBlank(token)) {
return ResultGenerator.genFailResult("未传token");
}
}
String tokenKey = RedisKey.getToken(token);
String systemToken = (String) redisUtil.get(tokenKey);
if (StringUtil.isEmpty(systemToken)) {
return ResultGenerator.genFailResult("请不要重复提交");
}
if (!systemToken.equals(token)) {
return ResultGenerator.genFailResult("无效token");
}
redisUtil.remove(tokenKey);
return ResultGenerator.genSuccessResult("");
}
}
package com.qdu.niit.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author hucs
* @date 2020/4/13 17:58
* @since JDK 1.8
*/
//方法级注解
@Target(ElementType.METHOD)
//运行时有效
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
}
package com.qdu.niit.config;
import com.alibaba.fastjson.JSON;
import com.qdu.niit.annotation.AutoIdempotent;
import com.qdu.niit.core.Result;
import com.qdu.niit.exception.CustomException;
import com.qdu.niit.util.token.TokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
/**
* @author [email protected]
* @version 0.5
* @date Created in 2020-04-13 18:34
* @description 描述
* @modified By
*/
@Component
@Slf4j
public class AutoIdempotentInterceptor implements HandlerInterceptor {
@Autowired
private TokenUtil tokenUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//被ApiIdempotment标记的扫描
AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
if (methodAnnotation != null) {
Result result = tokenUtil.checkToken(request);
if (result.getCode().equals("0000")) {
return true;
} else {
throw new CustomException(7777, result.getMessage());
}
}
return true;
}
//必须返回true,否则会被拦截一切请求
private void responseResult(HttpServletResponse response, Result result) {
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-type", "application/json;charset=UTF-8");
response.setStatus(200);
try {
response.getWriter().write(JSON.toJSONString(result));
} catch (IOException ex) {
log.error(ex.getMessage());
}
}
public boolean isAjaxRequest(HttpServletRequest request) {
String requestType = request.getHeader("X-Requested-With");
//如果requestType能拿到值,并且值为 XMLHttpRequest ,表示客户端的请求为异步请求,那自然是ajax请求了,反之如果为null,则是普通的请求
if (requestType == null) {
return false;
}
return true;
}
}
package com.qdu.niit.config;
import com.alibaba.fastjson.JSON;
import com.qdu.niit.annotation.AutoIdempotent;
import com.qdu.niit.core.Result;
import com.qdu.niit.exception.CustomException;
import com.qdu.niit.util.token.TokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
/**
* @author [email protected]
* @version 0.5
* @date Created in 2020-04-13 18:34
* @description 描述
* @modified By
*/
@Component
@Slf4j
public class AutoIdempotentInterceptor implements HandlerInterceptor {
@Autowired
private TokenUtil tokenUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//被ApiIdempotment标记的扫描
AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
if (methodAnnotation != null) {
Result result = tokenUtil.checkToken(request);
if (result.getCode().equals("0000")) {
return true;
} else {
throw new CustomException(7777, result.getMessage());
}
}
return true;
}
//必须返回true,否则会被拦截一切请求
private void responseResult(HttpServletResponse response, Result result) {
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-type", "application/json;charset=UTF-8");
response.setStatus(200);
try {
response.getWriter().write(JSON.toJSONString(result));
} catch (IOException ex) {
log.error(ex.getMessage());
}
}
public boolean isAjaxRequest(HttpServletRequest request) {
String requestType = request.getHeader("X-Requested-With");
//如果requestType能拿到值,并且值为 XMLHttpRequest ,表示客户端的请求为异步请求,那自然是ajax请求了,反之如果为null,则是普通的请求
if (requestType == null) {
return false;
}
return true;
}
}
//拿到token
$.ajax({
url: '/get/token',
type: 'get',
dataType: "json",
success: function (res) {
if (res.code == 0000) {
//将拿到的token赋值隐藏域,其实没什么必要,定义变量存储更好
$('#addToken').val(res.data);
} else {
layer.msg(res.message);
}
}
});
//
//提交表单
$.ajax({
url: '/console/user',
type: 'post',
data: JSON.stringify(data.field),
beforeSend: function (request) {
request.setRequestHeader("Authorization", $('#addToken').val());
},
dataType: "json",
contentType: 'application/json;charset=utf-8',
success: function (res) {
if (res.code == 0000) {
layer.msg('添加成功');
//清空表单
$('#addForm')[0].reset();
layer.close(divindex);
$('.layui-laypage-btn').click();
} else {
layer.msg(res.message);
}
}
});
表单第一次提交