關(guān)于表單的提交相信作為一個(gè)后端開發(fā)接觸過不少,本文將介紹如何解決表單重復(fù)提交的問題。
1、表單提交案例
我們通過一個(gè) jsp 頁面提交表單到 servlet 進(jìn)行處理。項(xiàng)目結(jié)構(gòu)如下:
首先看 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 {
//模擬網(wǎng)絡(luò)延時(shí)
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("提交表單");
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().print("提交成功?。?!");
}
}
我們將該項(xiàng)目部署到 tomcat 服務(wù)器,然后啟動(dòng)服務(wù)器,在瀏覽器中輸入相應(yīng)地址,點(diǎn)擊表單中的提交按鈕,后臺(tái)正常情況下應(yīng)該打印出提交表單的字樣,然后前臺(tái)頁面輸出提交成功。
2、表單重復(fù)提交的三種情況
上面我們演示的是正常點(diǎn)擊提交的情況,但是實(shí)際上用戶可能進(jìn)行多次提交的操作。
①、多次點(diǎn)擊提交按鈕
這是最明顯的一種情況,可能由于我們點(diǎn)擊一次按鈕后,系統(tǒng)后臺(tái)對(duì)提交操作進(jìn)行處理有一定的延時(shí),于是頁面停在表單提交頁面。而當(dāng)前用戶不知道,以為沒有提交表單,于是又進(jìn)行按鈕點(diǎn)擊,造成表單多次提交。
②、用戶提交表單成功之后不斷點(diǎn)擊瀏覽器【刷新】按鈕
③、提交表單成功后,點(diǎn)擊瀏覽器【回退】箭頭,回到表單提交頁面,然后重新點(diǎn)擊提交按鈕
3、前端解決辦法
①、onsubmit() 方法
在表單中增加onsubmit() 方法,該方法在表單提交時(shí)觸發(fā),返回false時(shí),表單就不會(huì)被提交。針對(duì)用戶多次點(diǎn)擊按鈕提交的問題,我們?cè)谇岸丝刂票韱翁峤灰淮沃?,?onsubmit() 方法返回值改為false,那么第二次點(diǎn)擊提交按鈕,表單將不能進(jìn)行提交。
< %@ 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 >
②、表單提交之后,將按鈕設(shè)置不可點(diǎn)擊
function dosubmit(){
//獲取表單提交按鈕
var btnSubmit = document.getElementById("submit");
//將表單提交按鈕設(shè)置為不可用,這樣就可以避免用戶再次點(diǎn)擊提交按鈕
btnSubmit.disabled= "disabled";
//返回true讓表單可以正常提交
return true;
}
存在問題:前面這兩種方法只能應(yīng)對(duì)用戶多次點(diǎn)擊提交按鈕的情況,也就是上面的第一種情況。但是對(duì)于提交之后多次刷新以及點(diǎn)擊回退按鈕,再次提交的這兩種情況卻沒有效果。這時(shí)候就需要在后端進(jìn)行解決。
4、后端解決
具體做法:
在服務(wù)器端生成一個(gè)唯一的隨機(jī)標(biāo)識(shí)號(hào),專業(yè)術(shù)語稱為Token(令牌),同時(shí)在當(dāng)前用戶的Session域中保存這個(gè)Token。然后將Token發(fā)送到客戶端的Form表單中,在Form表單中使用隱藏域來存儲(chǔ)這個(gè)Token,表單提交的時(shí)候連同這個(gè)Token一起提交到服務(wù)器端,然后在服務(wù)器端判斷客戶端提交上來的Token與服務(wù)器端生成的Token是否一致,如果不一致,那就是重復(fù)提交了,此時(shí)服務(wù)器端就可以不處理重復(fù)提交的表單。如果相同則處理表單提交,處理完后清除當(dāng)前用戶的Session域中存儲(chǔ)的標(biāo)識(shí)號(hào)。
在下列情況下,服務(wù)器程序?qū)⒕芙^處理用戶提交的表單請(qǐng)求:
1、存儲(chǔ)Session域中的Token(令牌)與表單提交的Token(令牌)不同。(包括偽造Token)
2、當(dāng)前用戶的Session中不存在Token(令牌)。
3、用戶提交的表單數(shù)據(jù)中沒有Token(令牌)。
①、首先通過服務(wù)器端的 servlet 跳轉(zhuǎn)到表單提交頁面:
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);
}
}
②、表單頁面增加隱藏域存儲(chǔ)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");
//將表單提交按鈕設(shè)置為不可用,這樣就可以避免用戶再次點(diǎn)擊提交按鈕
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 >
③、提交表單,后端進(jìn)行是否重復(fù)判斷
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("請(qǐng)不要重復(fù)提交!??!");
return;
}
try {
//模擬網(wǎng)絡(luò)延時(shí)
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("提交表單");
resp.getWriter().print("提交成功?。。?);
}
private boolean isRepeatSubmit(HttpServletRequest request){
//1、獲取存儲(chǔ)在request域中的tokenId
String req_tokenId = request.getParameter("tokenId");
//req_tokenId == null 表示表單中沒有token,即用戶不是通過servlet跳轉(zhuǎn)到該頁面或者是重復(fù)提交
if(req_tokenId == null) {
return true;
}
//2、獲取存儲(chǔ)在session域中的tokenId
String session_tokenId = (String) request.getSession().getAttribute("tokenId");
//如果當(dāng)前session域中的tokenId為null,則表示用戶重復(fù)提交(每次提交之后會(huì)移除該session域中的tokenId)
if(session_tokenId == null){
return true;
}
//3、存儲(chǔ)在session域中的tokenId和表單隱藏域保存提交的tokenId不同,則表示用戶偽造tokenId或者重復(fù)提交
if(!session_tokenId.equals(req_tokenId)){
return true;
}
//移除session域中的tokenId
request.getSession().removeAttribute("tokenId");
return false;
}
}
上面主要是利用一次回話中session域存儲(chǔ)的數(shù)據(jù)是保持不變的,而request域只能保存一次請(qǐng)求的數(shù)據(jù)。
注意:頁面首先要通過 servlet 進(jìn)行跳轉(zhuǎn)過去,不能直接訪問jsp頁面。先在 servlet 中生成一個(gè) tokenId,然后將tokenId存入到session域中,在轉(zhuǎn)發(fā)到j(luò)sp表單頁面,在表單頁面中,通過隱藏域存放生成的tokenId,然后點(diǎn)擊提交按鈕,會(huì)將隱藏域的tokenId 也一起提交到后端。后端首先判斷表單中的tokenId值,以及和session域中的tokenId 值進(jìn)行對(duì)比,表單中的tokenId為null,則說明是直接訪問的jsp頁面,session域中的tokenId 為null,則說明不是第一次提交,因?yàn)榈谝淮翁峤怀晒χ髸?huì)清空session域中的tokenId。都不為null,且兩者不相等,則說明可能是偽造的tokenId;不為null,且相等,則說明是第一次提交。
這里要注意銷毀session域中的tokenId時(shí)機(jī),是在判斷完是否重復(fù)提交的方法中最后就銷毀了,這樣可以防止還沒銷毀session域中的tokenId,客戶端的請(qǐng)求又來了。
5、session共享問題
通過上面前后端的解決表單重復(fù)提交的問題,我們看似解決了,其實(shí)不然,對(duì)于各種分布式項(xiàng)目,為了解決高并發(fā)的問題,我們會(huì)將前端請(qǐng)求通過 nginx 負(fù)載到多個(gè)tomcat服務(wù)器,如下:
這里會(huì)存在這樣一個(gè)問題:
首先通過 tomcat1 將請(qǐng)求跳轉(zhuǎn)到表單頁面,這時(shí)候tokenId 是存放在tomcat1 session域中,然后點(diǎn)擊提交按鈕,nginx 可能會(huì)將我們的請(qǐng)求分發(fā)到 tomcat2 上,而tomcat2 的session 域中是不存在 tokenId 的,這時(shí)候我們提交不了表單。
這也是session共享問題。也就是說我們必須找到一個(gè)存放 tokenId 的公共介質(zhì),無論是哪個(gè)服務(wù)器去處理請(qǐng)求,都是從公共介質(zhì)中獲取 tokenId,那么當(dāng)然不會(huì)存在tokenId 不一致的問題。
解決辦法:
①、利用數(shù)據(jù)庫同步:也就說將 tokenId 存放在數(shù)據(jù)庫中,每次獲取的時(shí)候從數(shù)據(jù)庫中查詢,這能解決,但是對(duì)數(shù)據(jù)的訪問壓力增大,不太合適。
②、利用 cookie 同步:因?yàn)?cookie 是存在本地客戶端的,第一次請(qǐng)求我們將tokenId 存放在cookie中,然后從cookie進(jìn)行是否重復(fù)提交校驗(yàn),這也能解決問題。但是cookie 存在安全性問題,而且每次http請(qǐng)求都要帶上參數(shù)也增加了帶寬消耗。
③、利用 Redis 同步:這是最好的一種辦法,Redis是一個(gè)高性能緩存框架,我們將 tokenId 存放在Redis中,獲取也從Redis中獲取,而且Redis性能極佳。
-
服務(wù)器
+關(guān)注
關(guān)注
12文章
9295瀏覽量
86001 -
開發(fā)
+關(guān)注
關(guān)注
0文章
370瀏覽量
40904 -
JSP
+關(guān)注
關(guān)注
0文章
26瀏覽量
10395 -
Servlet
+關(guān)注
關(guān)注
0文章
18瀏覽量
7901
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論