关于防止二重提交的解决方案以及对网站中使用这个解决方案出现的问题的分析
防止二重提交应用的场景:新建某个资料时(注意是新建不是编辑),当资料提交完毕返回到资料展示页面之后,点击浏览器的返回按钮进入新建页面或者确认画面,并点击页面里的登陆按钮,返回二重提交错误
解决方案:
form表单中加入隐藏的随机数+form表单提交后使提交按钮失效+禁用cache
详细说明
1.form表单中加入隐藏的随机数
这个是在刷新页面时,自动在页面里的所有的form表单中加入隐藏的含有随机数的input,同时把这个随机数存放到session中.
form提交后,在后台调用验证函数,看看提交的随机数和session中存的随机数是否相等并清空随机数,如果相等则通过,不相等则返回重复提交错误.
所以如果不刷新页面,无法再次提交
作用:1.在网络状况不好时,如果后台反应时间过长,前台的页面没有及时刷新,那么可以防止多次点击提交按钮(功能相当于form表单提交后使提交按钮失效)
2.在没有禁用缓存时,如果点击浏览器的向前,进入前一个画面,
此时画面为缓存,那么由于此页面的随机数没有更新,那么在此页面点击提交按钮时,会返回重复提交错误
2.form表单提交后使提交按钮失效
这个其实不是必须的,因为form表单中加入隐藏的随机数后,本来就无法在不刷新页面的情况下二重提交.
但是form表单中加入隐藏的随机数在二重提交时是返回错误,而form表单提交后使提交按钮失效则效果更直接,用户体验更好一些.
3.禁用cache
禁用cache之后,则点击浏览器的返回按钮时,页面会重新刷新,不用缓存,所以form表单是空的,通常来说也就不存在二重提交的问题,但是总有特殊情况.
[form表单中加入隐藏的随机数]使用流程:资料新建页面->(随机数验证,数据存入session)->资料确认页面->(随机数验证,若通过则把session中的数据存入数据库)->资料展示页面
对wellnessITadmin中使用这个解决方案出现的问题,分析如下:
现象:在使用"form表单中加入隐藏的随机数+form表单提交后使提交按钮失效+禁用cache"之后,出现了二重提交的现象.
流程:资料新建页面->(数据存入session)->资料确认页面->(随机数验证,若通过则session中的数据存入数据库)->资料展示页面
现象表现:在展示页面点击浏览器的返回按钮,进入表单确认页面,刷新页面,点击提交,则会提交和上次一样的数据,而且后台用过了验证.
现象分析:
1.由于禁用了cache,于是在展示页面点击浏览器的返回按钮进入表单确认页面时,无法使用浏览器的缓存,又因为资料确认页面是从资料新建页面以post方式过来的,浏览器默认不提交,只是提示再次刷新页面,刷新页面时,用的是post方式把新建页面的资料再次提交到了后台并存入session.此时展示的页面的表单里的随机数是更新后的,于是点击提交时,后台验证通过,又因为此时数据是在session里的,所以后台再次把session中的数据存入数据库,所以出现了二重提交.
解决方案:首先使用方法来说,"资料新建页面->(数据存入session)->资料确认页面"中缺少随机数验证,所以在从[资料新建页面]进入[资料确认页面]时,添加一次随机数验证即可解决这个问题.
因为当添加了随机数验证("资料新建页面->(随机数验证,数据存入session)->资料确认页面")之后,在展示页面点击浏览器的返回按钮进入表单确认页面时,浏览器默认不提交,只是提示再次刷新页面,此时刷新页面,用post方式把新建页面的资料再次提交到了后台,但是由于表单中的随机数已经失效,所以不会存入session,而是直接返回错误画面.
备注:禁用cache之后,从资料展示页面点击返回按钮进入资料确认页面时,如果浏览器显示一个请刷新(另外经wireshark抓包验证,从资料展示页面点击返回按钮进入资料确认画面时,浏览器除了在画面展示一个请刷新之外,没有任何访问记录),是因为资料确认页面是从资料新建页面以post方式过来的,浏览器默认不提交.
代码:
AbstractController.java中
public static final String RANDOMKEY = "randomKey";
public void checkHidenInput(){
String nowRandomKey = getRequest().getParameter("__randomKey");
String preRandomKey = (String)getSession().getAttribute(RANDOMKEY);
if(StringUtils.isEmpty(nowRandomKey)||StringUtils.isEmpty(preRandomKey)||!nowRandomKey.equals(preRandomKey)){
throw new DoubleSubmitException(getI18nMessage("errors.double.submit"));
}else{
getSession().removeAttribute(RANDOMKEY);
}
}
jsp中(一般放到共用的jsp模板里)
<%
String randomKey = java.util.UUID.randomUUID().toString();
session.setAttribute(tss.kz.controllers.AbstractController.RANDOMKEY, randomKey);
%>
<input type="hidden" id="__randomKey" name ="__randomKey" value="<%=randomKey%>"/>
js中(一般放到共用的js函数里)
$(document).ready(function(){
$.insertHidenInput();
})
jQuery.extend({
insertHidenInput:function(){
var hidenInput = $("#__randomKey").remove();
$("form").append(hidenInput);
}
});
使用如下:直接在相应的处理器里调用下面这个函数即可.
checkHidenInput();
代码分析:
之所以把
<%
String randomKey = java.util.UUID.randomUUID().toString();
session.setAttribute(tss.kz.controllers.AbstractController.RANDOMKEY, randomKey);
%>
写到jsp里,是因为如果写到abstractController里,ajax请求会刷新randomKey.但是由于可以在ajax共同函数里为所有的ajax请求加入自制头,所以其实可以在abstractController验证该请求是否为ajax,之后再进行判断是否更新randomKey.我想改的主要原因是不喜欢把处理逻辑写在jsp里,纯属个人意见.