在Web開發中,對於處理表單重復提交是經常要面對的事情。那么,存在哪些場景會導致表單重復提交呢?表單重復提交會帶來什么問題?有哪些方法可以避免表單重復提交?
表單重復提交的場景
1.場景一:服務端未能及時響應結果(網絡延遲,並發排隊等因素),導致前端頁面沒有及時刷新,用戶有機會多次提交表單
2.場景二:提交表單成功之后用戶再次點擊刷新按鈕導致表單重復提交
3.場景三:提交表單成功之后點擊后退按鈕回退到表單頁面再次提交
表單重復提交的弊端
下面通過一個簡單的示例進行說明。
- 表單頁面: test-form-submit-repeat.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>處理表單重復提交</title>
</head>
<body>
<form action="<%=request.getContextPath()%>/formServlet.do" method="post">
姓名:<input type="text" name="username" />
<input type="submit" value="提交">
</form>
</body>
</html>
- 后台Serlvet:FormServlet.java
public class FormServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
String userName = req.getParameter("username");
try {
// 模擬服務端處理效率慢
Thread.sleep(3 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("插入數據:" + userName);
}
}
實驗表單重復提交結果:
顯然,從演示結果來看,如果出現表單重復提交,將會導致相同的數據被重復插入到數據庫中。實際上,這是不應該發生的。
如何避免重復提交表單
關於解決表單重復提交,分為在前端攔截和服務端攔截2種方式。
1.在前端對表單重復提交進行攔截
在前端攔截表單重復提交可以通過多種方式實現:
(1)通過設置變量標志位進行攔截
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>處理表單重復提交</title>
</head>
<body>
<form action="<%=request.getContextPath()%>/formServlet.do" method="post" onsubmit="return checkSubmit();">
姓名:<input type="text" name="username" />
<input type="submit" value="提交">
</form>
</body>
<script type="text/javascript">
// 設置表單提交標志
var submit = false;
function checkSubmit() {
if(!submit) {
// 表單提交后設置標志位
submit = true;
return true;
}
// 表單已經提交,不允許再次提交
console.log("請不要重復提交表單!");
return false;
}
</script>
</html>
(2)通過禁用按鈕進行攔截
除了在前端通過設置標志位進行攔截之外,還可以在表單提交之后將按鈕disabled掉,這樣就徹底阻止了表單被重復提交的可能。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>處理表單重復提交</title>
</head>
<body>
<form action="<%=request.getContextPath()%>/formServlet.do" method="post" onsubmit="return disabledSubmit();">
姓名:<input type="text" name="username" />
<input id="submitBtn" type="submit" value="提交">
</form>
</body>
<script type="text/javascript">
function disabledSubmit() {
// 在提交按鈕第一次執行之后就disabled掉,避免重復提交
document.getElementById("submitBtn").disabled= "disabled";
return true;
}
</script>
</html>
當然,還可以直接在提一次提交之后將按鈕隱藏掉。但是,是否需要這樣做,需要考慮用戶的操作體驗是不是可以接受。
在前端攔截雖然可以解決場景一的表單重復提交問題,但是針對場景二(刷新)和場景三(后退重新提交)的表單重復提交是無能為力的。
2.在服務器端對表單重復提交進行攔截
在服務器端攔截表單重復提交的請求,實際上是通過在服務端保存一個token來實現的,而且這個在服務端保存的token需要通過前端傳遞,分三步走:
第一步:訪問頁面時在服務端保存一個隨機token
public class FormServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
UUID uuid = UUID.randomUUID();
String token = uuid.toString().replaceAll("-", "");
// 訪問頁面時隨機生成一個token保存在服務端session中
req.getSession().setAttribute("token", token);
req.getRequestDispatcher("/test-form-submit-repeat.jsp").forward(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
隨機token的產生可以使用任何恰當的方式,在這里通過UUID產生。
第二步:將服務端端保存的隨機token通過前端頁面傳遞
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>處理表單重復提交</title>
</head>
<body>
<form action="<%=request.getContextPath()%>/doFormServlet.do" method="post">
<!-- 隱藏域保存服務端token -->
<input type="hidden" name="token" value="<%=session.getAttribute("token")%>" />
姓名:<input type="text" name="username" />
<input id="submitBtn" type="submit" value="提交">
</form>
</body>
</html>
第三步:提交表單時在服務端通過檢查token來判斷是否為重復提交的表單請求
public class DoFormServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
if(checkRepeatSubmit(req)) {
System.out.println("請不要重復提交!");
return;
}
// 在第一次處理表單之后需要清空token,這一步非常關鍵
req.getSession().removeAttribute("token");
String userName = req.getParameter("username");
try {
// 模擬服務端處理效率慢
Thread.sleep(3 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("插入數據:" + userName);
}
// 檢查表單是否為重復提交
private boolean checkRepeatSubmit(HttpServletRequest req) {
Object sessionTokenObj = req.getSession().getAttribute("token");
if(sessionTokenObj == null) {
// 表單重復提交
System.out.println("Session token is NULL!");
return true;
}
String paramToken = req.getParameter("token");
if(paramToken == null) {
// 非法請求
System.out.println("Parameter token is NULL!");
return true;
}
if(!paramToken.equals(sessionTokenObj.toString())) {
// 非法請求
System.out.println("Token not same");
return true;
}
return false;
}
}
顯然,通過在服務端保存token的方式攔截場景二和場景三的表單重復提交是非常有效的。而且,這種方式同樣可以攔截場景一的表單重復提交。
也就是說,對於攔截表單重復提交的終極解決方案是在服務器端進行攔截!不過,考慮到用戶操作體驗的問題,可能需要同時在前端進行攔截,這可以根據具體的產品設計而定。
另外,有意思的是:在最新的Firefox瀏覽版本(Firefox Quantum 59.0.1 64位)中,瀏覽器自己就能處理場景一的表單重復提交(但是不能處理場景二和場景三的表單重復提交)。
經過驗證,在最新版的Chrome(Chrome 65.0.3325.181)瀏覽器中還不具備這個功能。