防止表单重复提交的方案:
一. 通过js限制表单是否可以提交的方式
PS:用户刷新页面或使用postman等工具绕过前段页面仍能重复提交表单。
1.form表单提交(第一种)
<form action="/acceptFormRequest/formRequest" onsubmit="return formSubmit()" method="post" target="nm_iframe">
<input type="hidden" id="formToken" name="formToken" >
用户名:<input type="text" name="submitInfo">
<input type="submit" value="提交" id="submit">
</form>
<iframe id="id_iframe" name="nm_iframe" style="display:none;"></iframe>
<script type="text/javascript">
//默认提交状态为false
var commitStatus = true;
function formSubmit(){
if(commitStatus){
//提交表单后,把提交状态改为false
commitStatus = false;
return true;//允许表单提交
}
return false;//阻止表单提交
}
</script>
(1.1)图中标红处,一定要加return ,主要是对form表单提交事件进行处理
(1.2)加iframe为了防止表单提交后跳转
2.form表单提交(第二种使用原生的ajax发送请求)
<form id="registSubmit" action="" method="post" >
<input type="hidden" name="formToken" value="${sessionScope.formToken}">
用户名:<input type="text" name="submitInfo" id="submitInfo">
<input type="submit" value="提交" id="submit">
</form>
<script type="text/javascript">
//默认提交状态为false
var commitStatus = true;
var registSubmitId = document.getElementById("registSubmit");
registSubmitId.onsubmit=function (ev) {
if(commitStatus) {
ajaxRequest();
commitStatus=false;
}
//一直防止表单提交,然后进行页面跳转 下边两种效果一样
//return false;
ev.preventDefault();
}
//ajax请求
function ajaxRequest() {
//1.创建XMLHTTPRequest对象,对于低版本的IE,需要换一个ActiveXObject对象
var xmlhttp;
if (window.XMLHttpRequest) {
xmlhttp = new XMLHttpRequest();
} else {
xmlhttp = new ActiveXObject('Microsoft.XMLHTTP');
}
//2.使用open方法设置和服务器的交互信息:
//设置请求的url参数,参数一是请求的类型,参数二是请求的url,参数三指定是否使用异步,默认是true
xmlhttp.open("post", "/acceptFormRequest/formRequest", true);
/*
1. post请求一定要添加请求头才行不然会报错
表单上传编码格式为application/x-www-form-urlencoded,参数的格式为key=value&key=value。
application/x-www-form-urlencoded是浏览器默认的编码格式。对于Get请求,
是将参数转换?key=value&key=value格式,连接到url后
服务器知道参数用符号&间隔,如果参数值中需要&,则必须对其进行编码。
编码格式就是application/x-www-form-urlencoded
(将键值对的参数用&连接起来,如果有空格,将空格转换为+加号;有特殊符号,
将特殊符号转换为ASCII HEX值)。
2.上传文件也要指定编码格式为multipart/form-data。
在form标签加属性 enctype="multipart/form-data"
注意:一般情况下使用“application/x-www-form-urlencoded”会比较快捷
大数据传输时一般选择“multipart/form-data”。
(PS:application/x-www-form-urlencoded和multipart/form-data两种在后台接收参数的时候有差异,
servlet,springMVC,springBoot接收需要注意配置问题下次再分享)
*/
xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
//3.发送请求 GET请求不需要参数,POST请求需要把body部分以字符串或者FormData对象传进去。
var submitInfoId = document.getElementById("submitInfo");
//和axios的格式一样
var params = "submitInfo="+submitInfoId.value;
xmlhttp.send(params);
//4.注册事件 onreadystatechange 状态改变就会调用
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState === 4) { // 成功完成
// 判断响应结果:
if (xmlhttp.status === 200) {
// 成功,通过responseText拿到响应的文本:
console.log(xmlhttp.responseText);
} else {
// 失败,根据响应码判断失败原因:
console.log(xmlhttp.status);
}
} else {
// HTTP请求还在继续...
}
}
}
</script>
2.form表单提交(第三种使用jquery.js的ajax或者jquery.form.min.js的ajaxForm或ajaxSubmit发送请求)
<form id="registSubmit" action="" method="post" >
<input type="hidden" name="formToken" value="${sessionScope.formToken}">
用户名:<input type="text" name="submitInfo" id="submitInfo">
<input type="submit" value="提交">
</form>
<script>
/**************************第一种使用ajax进行发送请求*****************************************/
//默认提交状态为true
var commitStatus = true;
$("#registSubmit").submit(function (e) {
if(commitStatus) {
ajaxRequest();
commitStatus=false;
}
//防止表单提交,然后进行页面跳转 下边两种效果一样
//return false;
e.preventDefault();
});
//发送ajax请求
function ajaxRequest() {
//1.
$.ajax({
url: "/acceptFormRequest/formRequest",
data: {submitInfo: $("#submitInfo").val()},
type: "POST",
dataType: "json",
success: function(data) {
// data = jQuery.parseJSON(data);
//dataType指明了返回数据为json类型,故不需要再反序列化
console.log(data);
}
});
/*
2.
$.post("/formSubmit", {submitInfo: $("#submitInfo").val()}, function(data) {
console.log(data);
},
"json"
);
*/
/*
3.
$.get("/formSubmit", function(data) {
console.log(data);
},
'json'
);
*/
}
/********************************第二种使用ajaxForm发送form表单**************************************************/
/* //表单默认可以提交
var commitStatus = true;
//返回false表单不会提交,可进行表单验证操作
function showRequest(formData, jqForm, options) {
//formData: 数组对象,提交表单时,Form插件会以Ajax方式自动提交这些数据,格式如:[{name:user,value:val },{name:pwd,value:pwd}]
//jqForm: jQuery对象,封装了表单的元素
//options: options对象
console.log(jqForm);
var queryString = $.param(formData); //name=1&address=2
var formElement = jqForm[0]; //将jqForm转换为DOM对象
console.log(queryString);
console.log(formElement);
//var address = formElement.submitInfo.value; //访问jqForm的DOM元素
if(commitStatus) {
commitStatus=false;
return true;
}
return false;
}
function showResponse(response, statusText) {
console.log(response);
}
var options = {
url:"/acceptFormRequest/formRequest",
type:'post',
target:'#registSubmit',//返回的内容更换指定的页面元素的内容 缺省值: null
beforeSerialize:function () {},//序列化提交数据之前的回调函数
beforeSubmit : showRequest,//提交之前执行的函数
uploadProgress:function(event, position, total, percentComplete){},//可以对文件上传进度监听
success :showResponse ,// 成功后执行的函数
error:function(){}, //提交失败执行的函数
dataType:'json', //服务器返回数据类型
clearForm:true, //提交成功后是否清空表单中的字段值
resetForm:true, //提交成功后是否重置表单中的字段值,即恢复到页面加载时的状态
timeout:3000, //设置请求时间,超过该时间后,自动退出请求,单位(毫秒)。
contentType: false, //不设置内容类型 如果上传文件下边的都不要设置
processData: false, //不处理数据
//
};
/!*
利用ajaxForm提交
options:也可以为一个回调函数
*!/
$("#registSubmit").ajaxForm(options).submit(function (ev) {
ev.preventDefault();//阻止表单的提交事件
});
/!*
ajaxForm() 不能主动提交 form(最后还是调用ajaxSubmit),函数只是为提交表单做准备需要以 submit 来触发提交;而 ajaxSubmit() 会主动提交表单,同时可以在点击其他按钮时也可以触发提交,不一定是submit按钮
*!/
$("#registSubmit").submit(function (e) {
$("#registSubmit").ajaxSubmit({
type: '', // 提交方式 get/post
url: '', // 需要提交的 url
data: {
'param': ""
},
success: function(data) {
// data 保存提交后返回的数据,一般为 json 数据
// 此处可对 data 作相关处理
alert('提交成功!' + data);
}
});
$(this).resetForm(); // 提交后重置表单
//阻止表单的提交事件,两种方式任选其一
e.preventDefault();
// return false;
});
*/
</script>
二. 给数据库增加唯一键约束(简单粗暴)
alter table table_name add constraint constraint_name unique(column1, column2,…,column_n);
三、通过后台验证session中的token的方式进行防止重复提交
1.定义FormToken自定义注解类。
import java.lang.annotation.*;
@Inherited //如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FormToken {
/**
* 如果该页面需要重复验证产生token,加上该标记 置值为true
* @return
*/
boolean save() default false;
/**
* 如果需要验证是否是重复提交的请求,加上该标记 置值为true
* @return
*/
boolean remove() default false;
}
2.产生token页面的加载
/**
* @ClassName: RouteFowardController
* @Author: Clement
* @Date: 2019/8/24 22:30
* @Version: 1.0
* @Description: 路由转发的控制器
*/
@Controller
@RequestMapping("/routeForward")
public class RouteFowardController {
private final String ONE_PAGE="one";
private final String TWO_PAGE="two";
private final String THREE_PAGE="three";
/**
* 加入需要生成token标示的注解
* @param flag
* @return
*/
@RequestMapping("/formPageGoto")
@FormToken(save = true)
public String formPageGoto(String flag) {
if (flag.equals(ONE_PAGE)) {
return "/formSubmitTest1.jsp";
}else if (flag.equals(TWO_PAGE)) {
return "/formSubmitTest2.jsp";
}else if(flag.equals(THREE_PAGE)){
return "/formSubmitTest3.jsp";
}else {
return "/index.jsp";
}
}
}
3.验证重复提交的请求的加载
/**
* @ClassName: AcceptFormRequestController
* @Author: Clement
* @Date: 2019/8/24 22:37
* @Version: 1.0
* @Description: 接收form表单的控制器
*/
@RestController
@RequestMapping("/acceptFormRequest")
public class AcceptFormRequestController {
@RequestMapping("/formRequest")
@FormToken(remove=true)
public Map processFormRequest(Map params) {
HashMap<String, Object> map = new HashMap<>();
System.out.println("接收到的提交数据:"+params);
map.put("code",200);
map.put("msg","已经接收到请求");
return map;
}
}
4.拦截器的配置类
/**
* @ClassName: AvoidRepeatableCommitInterceptor
* @Author: Clement
* @Date: 2019/8/24 22:01
* @Version: 1.0
* @Description: 避免重复提交拦截器
*/
public class AvoidRepeatCommitInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
/*
HandlerMethod
封装了很多属性,在访问请求方法的时候可以方便的访问到方法、
方法参数、方法上的注解、所属类等并且对方法参数封装处理,
也可以方便的访问到方法参数的注解等信息
*/
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
FormToken annotation = method.getAnnotation(FormToken.class);
if (annotation != null) {
boolean needSaveSession = annotation.save();
System.out.println("needSaveSession:"+needSaveSession);
if (needSaveSession) {
request.getSession(false).setAttribute("formToken", UUID.randomUUID().toString());
}
boolean needRemoveSession = annotation.remove();
if (needRemoveSession) {
/*
调用验证是否是重复提交的方法进行验证
重复提交则返回true
*/
if (isRepeatSubmit(request)) {
//对拦截的请求作响应
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
HashMap<String, Object> map = new HashMap<>();
map.put("code",200);
map.put("msg","请求为重复请求已经被拦截");
JSONObject jsonObject = new JSONObject(map);
out.print(jsonObject);
return false;
}
//执行业务操作方法之前,进行session中的token删除,防止页面前进后退的时候进行重复提交和session中的token对比
request.getSession(false).removeAttribute("formToken");
}
}
return true;
}else {
return super.preHandle(request, response, handler);
}
}
/**
* 是否是重复提交判断
* @param request
* @return
*/
public boolean isRepeatSubmit(HttpServletRequest request){
String serverToken = (String) request.getSession(false).getAttribute("formToken");
//如果第一次提交,则session中的token不为空
if(serverToken==null) {
return true;
}
String clientToken = request.getParameter("formToken");
if (clientToken == null) {
return true;
}
//返回给用户的token和session中的token进行值对比,不同则重复提交
if (!serverToken.equals(clientToken)) {
return true;
}
return false;
}
}
5.application中记得配置拦截
<!-- 拦截器 -->
<mvc:interceptors>
<mvc:interceptor>
<!-- 拦截器需要拦截的路径 MVC中
/,所以访问的地址都由DispatcherServlet进行解析(但是经测试,jsp资源不会被这种方式拦截)
/* 拦截所有的url 不包括.jsp
/**拦截所有的URL和子路径 (包括静态资源) spring mvc 重点:但是可以拦截html html页面发出的请求会被dispatcher处理
jsp如果不放在WEB-INF文件下,spring mvc是无法拦截的 这种请情况下需要用最原始的servlet的Filter接口,具体可以参照
http://blog.csdn.net/lsx991947534/article/details/45499205
注意:jsp放在WEB-INF下才能不被用户访问到 拦截jsp指的是不能 请求 .do / -->
<mvc:mapping path="/**"/>
<!-- 放行的路径 -->
<mvc:exclude-mapping path="/js/**"/> <!-- 放行js文件夹路径下所有路径 -->
<!-- 配置拦截器的类 -->
<bean class="com.web.interceptor.AvoidRepeatCommitInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
注:使用到的jar和js
当然还有AOP的方式进行处理。。。。。此处省略一万代码
年轻就要醒着拼!!!