關于表單的提交相信作為一個后端開發接觸過不少,本文將介紹如何解決表單重復提交的問題。
1、表單提交案例
我們通過一個 jsp 頁面提交表單到 servlet 進行處理。項目結構如下:
首先看 JSP 頁面:from01.jsp
< %@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"% >
< %
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
+ path;
% >
< !DOCTYPE html >
< head >
< title >Title< /title >
< /head >
< body >
< form action="< %=basePath% >/toServlet01" method="post" >
用戶名:< input type="text" name="userName" >
< input type="submit" value="提交" id="submit" >
< /form >
< /body >
< /html >
接著我們看 servlet 操作:
package com.ys.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Create by YSOcean
*/
@WebServlet("/toServlet01")
public class FormServlet01 extends HttpServlet{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("userName");
try {
//模擬網絡延時
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("提交表單");
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().print("提交成功!!!");
}
}
我們將該項目部署到 tomcat 服務器,然后啟動服務器,在瀏覽器中輸入相應地址,點擊表單中的提交按鈕,后臺正常情況下應該打印出提交表單的字樣,然后前臺頁面輸出提交成功。
2、表單重復提交的三種情況
上面我們演示的是正常點擊提交的情況,但是實際上用戶可能進行多次提交的操作。
①、多次點擊提交按鈕
這是最明顯的一種情況,可能由于我們點擊一次按鈕后,系統后臺對提交操作進行處理有一定的延時,于是頁面停在表單提交頁面。而當前用戶不知道,以為沒有提交表單,于是又進行按鈕點擊,造成表單多次提交。
②、用戶提交表單成功之后不斷點擊瀏覽器【刷新】按鈕
③、提交表單成功后,點擊瀏覽器【回退】箭頭,回到表單提交頁面,然后重新點擊提交按鈕
3、前端解決辦法
①、onsubmit() 方法
在表單中增加onsubmit() 方法,該方法在表單提交時觸發,返回false時,表單就不會被提交。針對用戶多次點擊按鈕提交的問題,我們在前端控制表單提交一次之后,將 onsubmit() 方法返回值改為false,那么第二次點擊提交按鈕,表單將不能進行提交。
< %@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"% >
< %
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
+ path;
% >
< !DOCTYPE html >
< head >
< title >Title< /title >
< /head >
< script type="text/javascript" >
var isFlag = false;
function dosubmit(){
if(!isFlag){
isFlag = true;
return true;
}else{
return false;
}
}
< /script >
< body >
< form action="< %=basePath% >/toServlet01" method="post" onsubmit="return dosubmit()" >
用戶名:< input type="text" name="userName" >
< input type="submit" value="提交" id="submit" >
< /form >
< /body >
< /html >
②、表單提交之后,將按鈕設置不可點擊
function dosubmit(){
//獲取表單提交按鈕
var btnSubmit = document.getElementById("submit");
//將表單提交按鈕設置為不可用,這樣就可以避免用戶再次點擊提交按鈕
btnSubmit.disabled= "disabled";
//返回true讓表單可以正常提交
return true;
}
存在問題:前面這兩種方法只能應對用戶多次點擊提交按鈕的情況,也就是上面的第一種情況。但是對于提交之后多次刷新以及點擊回退按鈕,再次提交的這兩種情況卻沒有效果。這時候就需要在后端進行解決。
4、后端解決
具體做法:
在服務器端生成一個唯一的隨機標識號,專業術語稱為Token(令牌),同時在當前用戶的Session域中保存這個Token。然后將Token發送到客戶端的Form表單中,在Form表單中使用隱藏域來存儲這個Token,表單提交的時候連同這個Token一起提交到服務器端,然后在服務器端判斷客戶端提交上來的Token與服務器端生成的Token是否一致,如果不一致,那就是重復提交了,此時服務器端就可以不處理重復提交的表單。如果相同則處理表單提交,處理完后清除當前用戶的Session域中存儲的標識號。
在下列情況下,服務器程序將拒絕處理用戶提交的表單請求:
1、存儲Session域中的Token(令牌)與表單提交的Token(令牌)不同。(包括偽造Token)
2、當前用戶的Session中不存在Token(令牌)。
3、用戶提交的表單數據中沒有Token(令牌)。
①、首先通過服務器端的 servlet 跳轉到表單提交頁面:
package com.ys.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
/**
* Create by YSOcean
*/
@WebServlet("/toForm")
public class ToFromServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String tokenId = UUID.randomUUID().toString();
req.getSession().setAttribute("tokenId",tokenId);
req.getRequestDispatcher("from01.jsp").forward(req,resp);
}
}
②、表單頁面增加隱藏域存儲tokenId
< %@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" isELIgnored="false"% >
< %
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
+ path;
% >
< !DOCTYPE html >
< head >
< title >Title< /title >
< /head >
< script type="text/javascript" >
var isFlag = false;
/*function dosubmit(){
if(!isFlag){
isFlag = true;
return true;
}else{
return false;
}
}*/
function dosubmit(){
//獲取表單提交按鈕
var btnSubmit = document.getElementById("submit");
//將表單提交按鈕設置為不可用,這樣就可以避免用戶再次點擊提交按鈕
btnSubmit.disabled= "disabled";
//返回true讓表單可以正常提交
return true;
}
< /script >
< body >
< form action="< %=basePath% >/toServlet01" method="post" onsubmit="return dosubmit()" >
< input type="hidden" name="tokenId" value="${tokenId}" >
用戶名:< input type="text" name="userName" >
< input type="submit" value="提交" id="submit" >
< /form >
< /body >
< /html >
③、提交表單,后端進行是否重復判斷
package com.ys.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Create by YSOcean
*/
@WebServlet("/toServlet01")
public class FormServlet01 extends HttpServlet{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
String username = req.getParameter("userName");
Boolean flag = isRepeatSubmit(req);
if(flag){
resp.getWriter().print("請不要重復提交!!!");
return;
}
try {
//模擬網絡延時
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("提交表單");
resp.getWriter().print("提交成功!!!");
}
private boolean isRepeatSubmit(HttpServletRequest request){
//1、獲取存儲在request域中的tokenId
String req_tokenId = request.getParameter("tokenId");
//req_tokenId == null 表示表單中沒有token,即用戶不是通過servlet跳轉到該頁面或者是重復提交
if(req_tokenId == null) {
return true;
}
//2、獲取存儲在session域中的tokenId
String session_tokenId = (String) request.getSession().getAttribute("tokenId");
//如果當前session域中的tokenId為null,則表示用戶重復提交(每次提交之后會移除該session域中的tokenId)
if(session_tokenId == null){
return true;
}
//3、存儲在session域中的tokenId和表單隱藏域保存提交的tokenId不同,則表示用戶偽造tokenId或者重復提交
if(!session_tokenId.equals(req_tokenId)){
return true;
}
//移除session域中的tokenId
request.getSession().removeAttribute("tokenId");
return false;
}
}
上面主要是利用一次回話中session域存儲的數據是保持不變的,而request域只能保存一次請求的數據。
注意:頁面首先要通過 servlet 進行跳轉過去,不能直接訪問jsp頁面。先在 servlet 中生成一個 tokenId,然后將tokenId存入到session域中,在轉發到jsp表單頁面,在表單頁面中,通過隱藏域存放生成的tokenId,然后點擊提交按鈕,會將隱藏域的tokenId 也一起提交到后端。后端首先判斷表單中的tokenId值,以及和session域中的tokenId 值進行對比,表單中的tokenId為null,則說明是直接訪問的jsp頁面,session域中的tokenId 為null,則說明不是第一次提交,因為第一次提交成功之后會清空session域中的tokenId。都不為null,且兩者不相等,則說明可能是偽造的tokenId;不為null,且相等,則說明是第一次提交。
這里要注意銷毀session域中的tokenId時機,是在判斷完是否重復提交的方法中最后就銷毀了,這樣可以防止還沒銷毀session域中的tokenId,客戶端的請求又來了。
5、session共享問題
通過上面前后端的解決表單重復提交的問題,我們看似解決了,其實不然,對于各種分布式項目,為了解決高并發的問題,我們會將前端請求通過 nginx 負載到多個tomcat服務器,如下:
這里會存在這樣一個問題:
首先通過 tomcat1 將請求跳轉到表單頁面,這時候tokenId 是存放在tomcat1 session域中,然后點擊提交按鈕,nginx 可能會將我們的請求分發到 tomcat2 上,而tomcat2 的session 域中是不存在 tokenId 的,這時候我們提交不了表單。
這也是session共享問題。也就是說我們必須找到一個存放 tokenId 的公共介質,無論是哪個服務器去處理請求,都是從公共介質中獲取 tokenId,那么當然不會存在tokenId 不一致的問題。
解決辦法:
①、利用數據庫同步:也就說將 tokenId 存放在數據庫中,每次獲取的時候從數據庫中查詢,這能解決,但是對數據的訪問壓力增大,不太合適。
②、利用 cookie 同步:因為 cookie 是存在本地客戶端的,第一次請求我們將tokenId 存放在cookie中,然后從cookie進行是否重復提交校驗,這也能解決問題。但是cookie 存在安全性問題,而且每次http請求都要帶上參數也增加了帶寬消耗。
③、利用 Redis 同步:這是最好的一種辦法,Redis是一個高性能緩存框架,我們將 tokenId 存放在Redis中,獲取也從Redis中獲取,而且Redis性能極佳。
-
服務器
+關注
關注
12文章
9303瀏覽量
86061 -
開發
+關注
關注
0文章
370瀏覽量
40920 -
JSP
+關注
關注
0文章
26瀏覽量
10406 -
Servlet
+關注
關注
0文章
18瀏覽量
7908
發布評論請先 登錄
相關推薦
評論