在 Web 開發中,表單重複提交是常見且棘手的問題。使用者可能因網路延遲而連點提交按鈕,或在成功頁面重新整理、瀏覽器後退後再次送出,導致資料重複寫入資料庫。本文將系統性說明重複提交的成因,並以 Token(令牌)機制 為核心,從 JSP 內嵌寫法、Servlet 分離、工具類封裝三個階段,示範如何徹底解決此問題。
forward 轉發至結果頁,使用者按 F5 會重送最後一次 POST 請求。注意:若後退後重新整理表單頁面再送出,屬於新請求,非重複提交。Firefox 對同一網址重複送出有防呆機制。
原理與驗證碼類似——「先取得表單頁面,才能提交表單」。流程如下:
<%
String token = java.util.UUID.randomUUID().toString();
session.setAttribute("TOKEN_IN_SESSION", token);
%>
<h3>轉賬界面</h3>
<form action="/trans" method="post">
<input type="hidden" name="token" value="<%=token%>"/>
轉賬金額:<input type="text" name="money" min="1" required /><br/>
<input type="submit" value="轉賬" />
</form>
@WebServlet("/trans")
public class TransferServlet extends HttpServlet {
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
String token = req.getParameter("token");
String sessionToken = (String) req.getSession().getAttribute("TOKEN_IN_SESSION");
if (token != null && token.equals(sessionToken)) {
req.getSession().removeAttribute("TOKEN_IN_SESSION"); // 銷毀
String money = req.getParameter("money");
System.out.println("轉出金額:" + money);
out.print("轉出金額:" + money);
} else {
out.print("重複提交或非法請求!");
}
}
}
將 Token 產生與轉發邏輯移至 TransferPrepareServlet,JSP 使用 EL 表達式 ${token} 接收,關注點分離更清晰。
@WebServlet("/transfer")
public class TransferPrepareServlet extends HttpServlet {
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String token = UUID.randomUUID().toString();
req.getSession().setAttribute("TOKEN_IN_SESSION", token);
req.setAttribute("token", token);
req.getRequestDispatcher("/views/repeatsubmit/submit.jsp").forward(req, resp);
}
}
<h3>轉賬界面</h3>
<form action="/trans" method="post">
<input type="hidden" name="token" value="${token}"/>
轉賬金額:<input type="text" name="money" min="1" required /><br/>
<input type="submit" value="轉賬" />
</form>
public class TokenUtil {
private final static String TOKEN_IN_SESSION = "TOKEN_IN_SESSION";
/** 產生 Token 並存入 Session、Request */
public static void saveToken(HttpServletRequest req) {
String token = UUID.randomUUID().toString();
req.getSession().setAttribute(TOKEN_IN_SESSION, token);
req.setAttribute("token", token);
}
/** 驗證並銷毀 Token,成功回 true */
public static boolean validateToken(HttpServletRequest req, String tokenInRequest) {
String sessionToken = (String) req.getSession().getAttribute(TOKEN_IN_SESSION);
if (tokenInRequest != null && tokenInRequest.equals(sessionToken)) {
req.getSession().removeAttribute(TOKEN_IN_SESSION);
return true;
}
return false;
}
}
使用時:
// 表單準備階段
TokenUtil.saveToken(req);
// 提交處理階段
if (TokenUtil.validateToken(req, req.getParameter("token"))) {
// 合法業務邏輯
} else {
// 重複提交處理
}
Token 機制以「一次性憑證」確保表單提交的唯一性與時序性。從 JSP 內嵌到 Servlet 分離,再到工具類封裝,層層遞進體現了關注點分離與可複用性的設計原則。實務上建議直接採用階段三的工具類,或整合至攔截器/過濾器實現全域防重,進一步簡化業務代碼。