一、产生表单重复提交可能的情况
1. 由于服务器缓慢或网络延迟等原因,导致用户重复点击提交按钮。
2. 使用forward方式已经提交成功,再次刷新成功页面导致重复提交。
3. 已经提交成功,通过回退,再次点击提交按钮。
注意:
在firefox,重复提交到同一地址无效。
回退后,刷新表单页面,再次提交这时不是重复提交,而是发送新的请求。
使用redirect方式重定向到成功页面不会出现重复提交的问题。
二、解决方式
要避免重复刷新、重复提交、以及防止后退的问题的,需要区分如何处理以及在哪里处理。处理的方式无非是客户端或者服务器端两种。对于不同的位置处理的方式也是不同的,但是任何客户端(尤其是B/S端)的处理都是不可信任的,最好的也是最应该的是服务器端的处理方法。
1. 客户端处理
javascript只能处理服务器繁忙时的用户多次提交。
1.1 重复刷新、重复提交
方法一:设置一个变量,只允许提交一次。
<
script
language
=
"javascript"
>
var checkSubmitFlag = false;
function checkSubmit() {
if (checkSubmitFlag == true) {
return false;
}
checkSubmitFlag = true;
return true;
}
document.ondblclick = function docondblclick() {
window.event.returnValue = false;
}
document.onclick = function doc {
if (checkSubmitFlag) {
window.event.returnValue = false;
}
}
</
script
>
<
html:form
action
=
"myAction.do"
method
=
"post"
onsubmit
=
"return checkSubmit();"
>
方法二:将提交按钮或者image置为disable
1.2 防止用户后退
这里的方法是千姿百态,有的是更改浏览器的历史纪录的,比如使用window.history.forward()方法;有的是“用新页面的URL替换当前的历史纪录,这样浏览历史记录中就只有一个页面,后退按钮永远不会变为可用。”比如使用javascript:location.replace(this.href); event.returnValue=false;2. 服务器端处理
实现思路:
使用UUID生成随机数,借助session,使用令牌机制来实现。
服务器在每次产生的页面FORM表单中分配一个唯一标识号(这个唯一标识号可以使用UUID产生),将这个唯一标识号保存在该FORM表单中的一个隐藏域中。同时在当前用户的session域中保存该唯一标识符。当用户提交表单时,服务器接收程序比较FORM表单隐藏字段中的标识号和存储在当前用户session域中的标识号是否一致。如果一致,则处理表单数据,同时清除当前用户session域中存储的标识号。如果不一致,服务器接收程序不处理提交的表单请求。
使用以上方式会导致编辑页面只能有一个。解决方案是:为每一个提交的form表单增加一个属性提交区别来源于不同表单。
示例主要代码:
UUIDToken.java
package
cn.heimar.common.util;
import
java.util.UUID;
import
javax.servlet.http.HttpServletRequest;
import
javax.servlet.http.HttpSession;
public
class
UUIDToken {
public
static
final
String UUIDTOKEN_IN_REQUEST =
"UUIDTOKEN_IN_REQUEST"
;
public
static
final
String UUIDTOKEN_IN_SESSION =
"UUIDTOKEN_IN_SESSION"
;
public
static
final
String UUIDTOKEN_FORMID_IN_REQUEST =
"UUIDTOKEN_FORMID_IN_REQUEST"
;
private
static
UUIDToken instance =
new
UUIDToken();
private
UUIDToken(){
}
public
static
UUIDToken getInstance(){
return
instance;
}
/**
* 生成UUIDToken
* @return
*/
public
void
generateUUIDToken(HttpServletRequest req){
HttpSession session = req.getSession(
true
);
UUID uuid = UUID.randomUUID();
UUID uuid_form_id = UUID.randomUUID();
req.setAttribute(UUIDTOKEN_IN_REQUEST, uuid.toString());
req.setAttribute(UUIDTOKEN_FORMID_IN_REQUEST, uuid_form_id.toString());
session.setAttribute(uuid_form_id.toString(), uuid.toString());
}
/*
* 是否重复提交
* 思路:
* 1,如果没有session,验证失败
* 2,如果session中没有UUIDTOKEN_IN_SESSION,验证失败
* 3,如果session中的UUIDTOKEN_IN_SESSION和表单中的UUIDTOKEN_IN_REQUEST不相等,验证失败
*/
public
boolean
isRepeatSubmit(HttpServletRequest req){
//是否重复提交(默认true重复提交)
boolean
isRepeatSubmit =
true
;
HttpSession session = req.getSession(
false
);
if
(session !=
null
){
String uuidToken_formId = req.getParameter(
"UUIDTOKEN_FORMID_IN_REQUEST"
);
if
(StringUtil.isNotBlank(uuidToken_formId)){
String uuidToken_in_session = (String)session.getAttribute(uuidToken_formId);
if
(StringUtil.isNotBlank(uuidToken_in_session)){
String uuidToken_in_request = req.getParameter(UUIDTOKEN_IN_REQUEST);
if
(uuidToken_in_session.equals(uuidToken_in_request)){
isRepeatSubmit =
false
;
//清除session中的uuid防重复提交令牌
session.removeAttribute(uuidToken_formId);
}
}
}
}
return
isRepeatSubmit;
}
package
cn.heimar.demo.servlet;
import
java.io.IOException;
import
java.math.BigDecimal;
import
java.util.List;
import
java.util.UUID;
import
javax.servlet.ServletException;
import
javax.servlet.http.HttpServlet;
import
javax.servlet.http.HttpServletRequest;
import
javax.servlet.http.HttpServletResponse;
import
javax.servlet.http.HttpSession;
import
cn.heimar.common.core.dao.DaoFactory;
import
cn.heimar.common.util.PageResult;
import
cn.heimar.common.util.StringUtil;
import
cn.heimar.common.util.UUIDToken;
import
cn.heimar.demo.dao.IProductDao;
import
cn.heimar.demo.dao.ISupplierDao;
import
cn.heimar.demo.dao.query.ProductQueryObject;
import
cn.heimar.demo.domain.Product;
import
cn.heimar.demo.domain.Supplier;
public
class
ProductServlet
extends
HttpServlet {
private
static
final
long
serialVersionUID = -3393643900564582082L;
private
IProductDao productDao;
private
ISupplierDao supplierDao;
private
boolean
hasLength(String s) {
return
s !=
null
&& !
""
.equals(s.trim());
}
@Override
protected
void
service(HttpServletRequest req, HttpServletResponse resp)
throws
ServletException, IOException {
String cmd = req.getParameter(
"cmd"
);
if
(
"edit"
.equals(cmd)) {
// 转向添加页面
String id = req.getParameter(
"id"
);
if
(hasLength(id)) {
Product p = productDao.get(Long.parseLong(id));
if
(p !=
null
)
req.setAttribute(
"p"
, p);
}
List<Supplier> suppliers = supplierDao.getSimpleSupplier();
req.setAttribute(
"suppliers"
, suppliers);
//防止重复提交:
//在导向编辑页面时,向request和session域中添加uuid随机数
UUIDToken.getInstance().generateUUIDToken(req);
// 导向添加页面
req.getRequestDispatcher(
"/WEB-INF/views/demo/product/edit.jsp"
).forward(req, resp);
}
// 执行保存过程
else
if
(
"save"
.equals(cmd)) {
String id = req.getParameter(
"id"
);
Product p =
new
Product();
if
(hasLength(id)) {
p = productDao.get(Long.parseLong(id));
}
System.out.println(req.getParameter(
"supplier"
));
// 设置值
setProperties(req, p);
//判断是否重复提交
if
(!UUIDToken.getInstance().isRepeatSubmit(req)){
if
(p.getId() !=
null
) {
productDao.update(p);
}
else
{
productDao.save(p);
}
}
else
{
System.out.println(
"重复提交!"
);
}
//重定向
resp.sendRedirect(req.getContextPath() +
"/product"
);
}
else
{
if
(
"del"
.equals(cmd)) {
// 执行删除过程
String id = req.getParameter(
"id"
);
if
(hasLength(id)) {
productDao.delete(Long.parseLong(id));
}
}
// 列出所有货品(除了转向添加/编辑页面,其他都要执行这句)
// List<Product> ps = dao.getAll();
// req.setAttribute("ps", ps);
// req.getRequestDispatcher("/WEB-INF/jsp/list.jsp")
// .forward(req, resp);
ProductQueryObject pqo = createQueryObject(req);
// 高级查询
PageResult<Product> ps = productDao.getByQuery(pqo);
req.setAttribute(
"page"
, ps);
List<Supplier> suppliers = supplierDao.getSimpleSupplier();
req.setAttribute(
"suppliers"
, suppliers);
req.setAttribute(
"qo"
,pqo);
req.getRequestDispatcher(
"/WEB-INF/views/demo/product/list.jsp"
)
.forward(req, resp);
}
}
/**
* 创建高级查询对象
*
* @param req
* @return
*/
private
ProductQueryObject createQueryObject(HttpServletRequest req) {
ProductQueryObject pqo =
new
ProductQueryObject();
pqo.setName(req.getParameter(
"name"
));
pqo.setSn(req.getParameter(
"sn"
));
pqo.setBrand(req.getParameter(
"brand"
));
String supplier = req.getParameter(
"supplier"
);
if
(StringUtil.isNotBlank(supplier)){
pqo.setSupplier(Long.parseLong(supplier));
}
String salePrice1 = req.getParameter(
"salePrice1"
);
if
(hasLength(salePrice1))
pqo.setSalePrice1(
new
BigDecimal(salePrice1));
String salePrice2 = req.getParameter(
"salePrice2"
);
if
(hasLength(salePrice2))
pqo.setSalePrice2(
new
BigDecimal(salePrice2));
String costPrice1 = req.getParameter(
"costPrice1"
);
if
(hasLength(costPrice1))
pqo.setCostPrice1(
new
BigDecimal(costPrice1));
String costPrice2 = req.getParameter(
"costPrice2"
);
if
(hasLength(costPrice2))
pqo.setCostPrice2(
new
BigDecimal(costPrice2));
String currentPage = req.getParameter(
"currentPage"
);
if
(hasLength(currentPage)) {
pqo.setCurrentPage(Integer.parseInt(currentPage));
}
return
pqo;
}
/**
* 设置值
*
* @param req
* @param p
*/
private
void
setProperties(HttpServletRequest req, Product p) {
p.setName(req.getParameter(
"name"
));
p.setSn(req.getParameter(
"sn"
));
String supplier_id = req.getParameter(
"supplier"
);
if
(StringUtil.isNotBlank(supplier_id)){
Supplier s =
new
Supplier();
s.setId(Long.parseLong(supplier_id));
p.setSupplier(s);
}
p.setBrand(req.getParameter(
"brand"
));
p.setTypes(req.getParameter(
"types"
));
p.setUnit(req.getParameter(
"unit"
));
p.setSalePrice(
new
BigDecimal(req.getParameter(
"salePrice"
)));
p.setCostPrice(
new
BigDecimal(req.getParameter(
"costPrice"
)));
p.setCutoffPrice(
new
BigDecimal(req.getParameter(
"cutoffPrice"
)));
}
@Override
public
void
init()
throws
ServletException {
super
.init();
productDao = (IProductDao) DaoFactory.getDao(
"productDao"
);
supplierDao = (ISupplierDao) DaoFactory.getDao(
"supplierDao"
);
}
}
<%@ page language="java" pageEncoding="utf-8" contentType="text/html; charset=utf-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<
html
>
<
head
>
<
meta
http-equiv
=
"Content-Type"
content
=
"text/html; charset=utf-8"
/>
<
title
>编辑产品</
title
>
</
head
>
<
body
>
<
form
name
=
"form1"
action="<%=request.getContextPath() %>/product?cmd=save" method="post" >
<
input
type
=
"hidden"
name
=
"id"
value
=
"${p.id }"
>
<
input
type
=
"hidden"
name
=
"UUIDTOKEN_IN_REQUEST"
value
=
"${UUIDTOKEN_IN_REQUEST }"
>
<
input
type
=
"hidden"
name
=
"UUIDTOKEN_FORMID_IN_REQUEST"
value
=
"${UUIDTOKEN_FORMID_IN_REQUEST }"
>
<
table
width
=
"90%"
border
=
"0"
align
=
"center"
cellpadding
=
"4"
cellspacing
=
"2"
class
=
"gray-bd3-2"
>
<
tr
>
<
td
width
=
"100"
align
=
"center"
class
=
"brightcyan-tb"
>产品名称:</
td
>
<
td
><
input
name
=
"name"
type
=
"text"
class
=
"big-input"
maxLength
=
"20"
size
=
"20"
value
=
"${p.name }"
/></
td
>
<
tr
>
<
tr
>
<
td
width
=
"100"
align
=
"center"
class
=
"brightcyan-tb"
>产品编码:</
td
>
<
td
><
input
name
=
"sn"
type
=
"text"
class
=
"big-input"
maxLength
=
"20"
size
=
"20"
value
=
"${p.sn }"
/></
td
>
<
tr
>
<
tr
>
<
td
width
=
"100"
align
=
"center"
class
=
"brightcyan-tb"
>供应商:</
td
>
<
td
>
<
select
name
=
"supplier"
>
<
option
value
=
"0"
>--请选择--</
option
>
<
c:forEach
var
=
"o"
items
=
"${suppliers }"
>
<
option
value
=
"${o.id }"
<c:if
test
=
"${not empty id && p.supplier.id == o.id }"
>selected="selected"</
c:if
>>
${o.name }
</
option
>
</
c:forEach
>
</
select
>
<!-- <input name="supplier" type="text" class="big-input" maxLength="20" size="20" value="${p.supplier }"/> -->
</
td
>
<
tr
>
<
tr
>
<
td
width
=
"100"
align
=
"center"
class
=
"brightcyan-tb"
>品牌:</
td
>
<
td
><
input
name
=
"brand"
type
=
"text"
class
=
"big-input"
maxLength
=
"20"
size
=
"20"
value
=
"${p.brand }"
/></
td
>
<
tr
>
<
tr
>
<
td
width
=
"100"
align
=
"center"
class
=
"brightcyan-tb"
>分类:</
td
>
<
td
><
input
name
=
"types"
type
=
"text"
class
=
"big-input"
maxLength
=
"20"
size
=
"20"
value
=
"${p.types }"
/></
td
>
<
tr
>
<
tr
>
<
td
width
=
"100"
align
=
"center"
class
=
"brightcyan-tb"
>单位:</
td
>
<
td
><
input
name
=
"unit"
type
=
"text"
class
=
"big-input"
maxLength
=
"20"
size
=
"20"
value
=
"${p.unit }"
/></
td
>
<
tr
>
<
tr
>
<
td
width
=
"100"
align
=
"center"
class
=
"brightcyan-tb"
>零售价:</
td
>
<
td
><
input
name
=
"salePrice"
type
=
"text"
class
=
"big-input"
maxLength
=
"20"
size
=
"20"
value
=
"${p.salePrice }"
/></
td
>
<
tr
>
<
tr
>
<
td
width
=
"100"
align
=
"center"
class
=
"brightcyan-tb"
>成本价:</
td
>
<
td
><
input
name
=
"costPrice"
type
=
"text"
class
=
"big-input"
maxLength
=
"20"
size
=
"20"
value
=
"${p.costPrice }"
/></
td
>
<
tr
>
<
tr
>
<
td
width
=
"100"
align
=
"center"
class
=
"brightcyan-tb"
>折扣价:</
td
>
<
td
><
input
name
=
"cutoffPrice"
type
=
"text"
class
=
"big-input"
maxLength
=
"20"
size
=
"20"
value
=
"${p.cutoffPrice }"
/></
td
>
<
tr
>
</
table
>
<
p
align
=
"center"
>
<
input
name
=
"ok"
type
=
"button"
class
=
"small-btn"
value
=
"提交"
onClick
=
"save()"
/>
<
input
type
=
"reset"
class
=
"small-btn"
value
=
"重置"
/>
</
p
>
</
form
>
<
script
type
=
"text/javascript"
>
function save(){
//forms javaScrip的内置对象 表示form的集合 是一个数组
//提交表单
document.forms[0].submit();
}
</
script
>
</
body
>
</
html
>
3. struts的同步令牌
利用同步令牌(Token)机制来解决Web应用中重复提交的问题,Struts也给出了一个参考实现。
基本原理:服务器端在处理到达的请求之前,会将请求中包含的令牌值与保存在当前用户会话中的令牌值进行比较,
看是否匹配。在处理完该请求后,且在答复发送给客户端之前,将会产生一个新的令牌,该令牌除传给
客户端以外,也会将用户会话中保存的旧的令牌进行替换。这样如果用户回退到刚才的提交页面并再次
提交的话,客户端传过来的令牌就和服务器端的令牌不一致,从而有效地防止了重复提交的发生。