使用者雙擊提交按鈕、按 F5 重整、瀏覽器後退重送,都會導致重複提交。本文系統整理客戶端與伺服器端七種防重策略,並分析優缺點與適用場景。
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache, must-revalidate">
<meta http-equiv="Expires" content="0">
缺點:無法阻止使用者主動點擊、重整、後退;僅防止瀏覽器快取頁面。
<input type="submit" value="提交" onclick="this.disabled=true; this.form.submit();">
或使用旗標變數:
var submitted = false;
function checkSubmit() {
if (submitted) return false;
submitted = true;
return true;
}
<form onsubmit="return checkSubmit()">...
缺點:JS 被禁用或駭客繞過前端即失效。
處理完 POST 請求後,重導向 (302) 到結果頁(GET),而非直接 Forward:
// Servlet / Controller
response.sendRedirect(request.getContextPath() + "/result.jsp");
Struts 1 配置:
<forward name="success" path="/list.jsp" redirect="true"/>
原理:瀏覽器歷史記錄只保留 GET 請求,重整不會重送 POST。
缺點:需在 Session/Flash Scope 傳遞提示訊息;無法攔截「後退到表單頁再提交」。
流程:
<!-- JSP -->
<html:form action="/save"> ... </html:form>
<!-- 自動產生 <input type="hidden" name="org.apache.struts.taglib.html.TOKEN" value="..."> -->
// Action
if (isTokenValid(request, true)) { // true = 自動重置 token
// 業務邏輯
return mapping.findForward("success");
} else {
saveToken(request); // 重新生成供重試
return mapping.findForward("duplicate");
}
// 表單頁
String token = UUID.randomUUID().toString();
session.setAttribute("FORM_TOKEN", token);
// JSP: <input type="hidden" name="token" value="${sessionScope.FORM_TOKEN}">
// 處理 Servlet
String submittedToken = request.getParameter("token");
String sessionToken = (String) session.getAttribute("FORM_TOKEN");
if (sessionToken != null && sessionToken.equals(submittedToken)) {
session.removeAttribute("FORM_TOKEN"); // 或更新新 token
// 執行業務
} else {
// 重複提交處理
}
訂單號、流水號加 UNIQUE INDEX,重複寫入拋異常回滾。配合冪等性設計(如「支付狀態機」),確保同一筆業務只能成功一次。
$.ajax({
url: '/api/submit',
type: 'POST',
data: $('#form').serialize(),
beforeSend: function() { $('#btnSubmit').prop('disabled', true); },
complete: function() { $('#btnSubmit').prop('disabled', false); },
success: function(resp) { /* 處理結果 */ }
});
結合前端鎖定與後端 Token 雙重保險。
<script>
// 頁面載入時推進歷史,阻止後退
window.history.pushState(null, null, location.href);
window.onpopstate = function() { window.history.pushState(null, null, location.href); };
</script>
注意:此為體驗輔助,無法替代伺服器端驗證。
| 場景 | 推薦組合 |
|---|---|
| 一般表單(增刪改) | PRG + Session Token + DB 唯一約束 |
| 高併發下單/支付 | Token + 分散式鎖 (Redis) + 冪等鍵 |
| 純查詢/GET | 無需防重,加快取快取 |
| 舊系統快速修補 | JS 禁用按鈕 + PRG |
前端攔截只能誠實使用者,伺服器端 Token 才是核心防線。採用「PRG 模式 + Session Token + 資料庫唯一約束」三層防禦,可覆蓋 99% 重複提交場景。現代框架(Spring MVC、Struts 2、Spring Boot)皆內建 CSRF/Token 支援,優先使用框架機制,避免重複造輪子。