首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> JSP 程式設計 >> 關於JSP

JSP 表單重複提交防範全攻略:客戶端、伺服器端、Token 機制完整解析

     編輯:關於JSP

問題場景分類

場景觸發原因後果
用戶連點提交按鈕網路延遲、無回饋重複訂單、重複扣款
瀏覽器重新整理 (F5)POST 頁面重新整理重送請求重複業務邏輯執行
瀏覽器後退鍵返回表單頁快取頁面再次提交資料不一致、Token 失效
惡意腳本/爬蟲重放自動化工具資源耗盡、資料汙染

防禦層級策略

採用纵深防禦:客戶端攔截(體驗優化)+ 伺服器端驗證(核心保障)+ 業務冪等設計(根本解決)。

第一層:客戶端攔截(體驗優化,不可信)

1.1 按鈕鎖定 - 最簡單有效

<input type="submit" value="提交" 
       onclick="this.disabled=true; this.form.submit();" />
  • 提交瞬間鎖定按鈕,防止連點。
  • 缺點:JS 被禁用時失效;表單驗證失敗需解鎖(需額外邏輯)。

1.2 全域提交旗標

<script>
var submitted = false;
function checkSubmit() {
    if (submitted) return false;
    submitted = true;
    return true;
}
// 防雙擊
document.ondblclick = function(e) { e.preventDefault(); };
</script>

<form onsubmit="return checkSubmit();">...</form>

1.3 表單提交後導向 (PRG) - 解決 F5 重複

伺服器端處理完 POST 請求後,不直接渲染頁面,而是 sendRedirect 到 GET 頁面。

// Servlet/Action 處理完業務
response.sendRedirect("success.jsp"); // 或列表頁
return; // 必須 return
  • 瀏覽器歷史記錄替換為 GET 請求,F5 僅重送 GET,不觸發業務邏輯。
  • Struts 1 設定 <forward name="success" path="/list.jsp" redirect="true"/>
  • Struts 2 回傳 "redirect:list.action" 或使用 redirectAction 結果類型。

第二層:伺服器端 Token 機制(核心防禦)

2.1 原理圖解

[首次 GET]  → 產生 Token 存 Session + 隱藏欄位輸出
[用戶 POST] → 比對 Session Token 與表單 Token
    ├─ 一致 → 執行業務、刷新 Session Token、回傳結果
    └─ 不一致 → 拒絕、提示「重複提交」、重新發 Token

2.2 Struts 1 內建 Token 範例

<!-- JSP -->
<html:form action="/order">
    <!-- 自動產生 <input type="hidden" name="org.apache.struts.taglib.html.TOKEN" value="..."/> -->
</html:form>

// Action
public ActionForward execute(ActionMapping mapping, ActionForm form,
                             HttpServletRequest req, HttpServletResponse resp) {
    if (!isTokenValid(req, true)) { // true = 驗證後重置
        saveToken(req); // 重新產生 Token 供下次使用
        return mapping.findForward("duplicate"); // 導向錯誤頁
    }
    // 正常業務邏輯
    return mapping.findForward("success");
}

2.3 手動實作通用 TokenFilter(框架無關)

@WebFilter("/secure/*")
public class TokenFilter implements Filter {
    private static final String TOKEN_KEY = "csrf_token";
    private static final String HEADER_NAME = "X-CSRF-Token";

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;

        if ("POST".equalsIgnoreCase(request.getMethod())) {
            HttpSession session = request.getSession(false);
            String sessionToken = session != null ? (String) session.getAttribute(TOKEN_KEY) : null;
            String requestToken = request.getParameter(TOKEN_KEY);
            // 也支援 Header 傳遞 (AJAX)
            if (requestToken == null) requestToken = request.getHeader(HEADER_NAME);

            if (sessionToken == null || !sessionToken.equals(requestToken)) {
                response.setStatus(HttpServletResponse.SC_CONFLICT); // 409
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write("{\"error\":\"重複提交或 Token 失效,請重新整理頁面\"}");
                return;
            }
            // 驗證通過:消費 Token,產生新 Token
            session.setAttribute(TOKEN_KEY, generateToken());
        }
        chain.doFilter(req, resp);
    }

    private String generateToken() {
        return UUID.randomUUID().toString().replace("-", "");
    }
}

2.4 JSP 頁面植入 Token

<% String token = (String) session.getAttribute("csrf_token");
   if (token == null) { token = UUID.randomUUID().toString().replace("-", ""); session.setAttribute("csrf_token", token); }
%>
<form method="post" action="...">
    <input type="hidden" name="csrf_token" value="<%= token %>"/>
    ...
</form>

第三層:業務冪等設計(根本解決)

即使請求重複到達,業務邏輯天然冪等,則無需額外防禦。

  • 唯一約束:資料庫表加 UNIQUE (user_id, order_no),重複插入拋異常回滾。
  • 狀態機:訂單狀態 NEW → PAID → SHIPPED,僅允許合法轉換。
  • 去重鍵:業務層接受 idempotency_key (UUID),Redis SETNX 檢查是否已處理。

防後退導航的補充方案

方案原理優/缺點
history.pushState() + popstate 攔截替換歷史記錄,阻止回退現代瀏覽器支援,但可被繞過
頁面 onbeforeunload 提示用戶嘗試離開時確認體驗較差,僅作輔助
Token 機制 (推薦)後退頁面 Token 已失效,提交被攔截核心防禦,不依賴瀏覽器行為

總結建議清單

  1. ✅ 所有表單啟用 伺服器端 Token 驗證(Filter 或框架內建)。
  2. ✅ 表單提交採用 PRG 模式 (POST → Redirect → GET)。
  3. ✅ 前端按鈕 提交即鎖定,提升體驗。
  4. ✅ 關鍵業務設計 冪等性 (唯一鍵、狀態機、去重鍵)。
  5. ✅ 敏感操作 (付款、刪除) 加入 二次確認 (Modal 對話框)。
  6. ❌ 單純依賴 JS、Meta no-cache、history 導航攔截 —— 均可繞過。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved