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

Java Web 防止表單重復提交:Token 令牌機制完整實現與工具類封裝

     編輯:Java 綜合教學

Java Web 防止表單重復提交:Token 令牌機制完整實現與工具類封裝

表單重復提交指未完成完整「請求頁面 → 提交表單」流程就多次提交。根本原因:缺乏請求唯一性標識。解決方案:令牌機制——頁面生成 Token,Session 存一份,表單攜帶提交,服務端驗證銷毀。

問題復現

@WebServlet("/trans")
public class TransferServlet extends HttpServlet {
    protected void service(HttpServletRequest req, HttpServletResponse resp) {
        // 模擬網絡延遲 3 秒
        Thread.sleep(3000);
        String money = req.getParameter("money");
        System.out.println("轉出金額: " + money);  // 瘋狂點擊會打印多次
    }
}

令牌機制核心流程

  1. 請求頁面:生成 UUID Token → 存入 Session → 放入 Model/Request → 頁面隱藏域輸出
  2. 提交表單:攜帶隱藏域 Token 發送
  3. 服務端驗證:比較請求 Token 與 Session Token → 一致則處理業務並刪除 Session Token → 不一致拒絕

完整實現代碼

1. JSP 頁面(使用 EL 表達式)

<%@ page contentType="text/html;charset=UTF-8" %>
<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>

2. 頁面跳轉 Servlet(生成 Token)

@WebServlet("/transfer")
public class TransferPageServlet extends HttpServlet {
    protected void service(HttpServletRequest req, HttpServletResponse resp) {
        String token = UUID.randomUUID().toString();
        req.getSession().setAttribute("TOKEN_IN_SESSION", token);
        req.setAttribute("token", token);
        req.getRequestDispatcher("/views/submit.jsp").forward(req, resp);
    }
}

3. 業務 Servlet(驗證 Token)

@WebServlet("/trans")
public class TransferServlet extends HttpServlet {
    protected void service(HttpServletRequest req, HttpServletResponse resp) {
        String token = req.getParameter("token");
        String sessionToken = (String) req.getSession().getAttribute("TOKEN_IN_SESSION");
        
        if (token != null && token.equals(sessionToken)) {
            // 驗證通過:刪除 Token,防止重復提交
            req.getSession().removeAttribute("TOKEN_IN_SESSION");
            
            String money = req.getParameter("money");
            System.out.println("轉出金額: " + money);
            // 業務邏輯...
        } else {
            // 重復提交或 Token 失效
            resp.getWriter().write("請勿重復提交");
        }
    }
}

4. 工具類封裝(推薦:消除樣板代碼)

public class TokenUtil {
    private static final String TOKEN_KEY = "TOKEN_IN_SESSION";
    
    // 生成並存儲 Token
    public static void saveToken(HttpServletRequest req) {
        String token = UUID.randomUUID().toString();
        req.getSession().setAttribute(TOKEN_KEY, token);
        req.setAttribute("token", token);  // 供頁面使用
    }
    
    // 驗證並銷毀 Token
    public static boolean validateToken(HttpServletRequest req, String requestToken) {
        String sessionToken = (String) req.getSession().getAttribute(TOKEN_KEY);
        if (requestToken != null && requestToken.equals(sessionToken)) {
            req.getSession().removeAttribute(TOKEN_KEY);
            return true;
        }
        return false;
    }
}

5. Controller 中使用工具類

@GetMapping("/transfer")
public String showPage(HttpServletRequest req) {
    TokenUtil.saveToken(req);
    return "transfer";
}

@PostMapping("/trans")
public String transfer(HttpServletRequest req, @RequestParam String money) {
    String token = req.getParameter("token");
    if (!TokenUtil.validateToken(req, token)) {
        return "error/repeat-submit";
    }
    // 業務處理...
    return "success";
}

進階優化

  • 分佈式環境:Token 存 Redis,Key 為用戶ID+業務標識,TTL 30分鐘
  • 幂等性設計:業務層面基於業務主鍵(訂單號)天然幂等,Token 為輔助
  • 前端防抖:按鈕提交後置灰禁用,JS 防止多次點擊發請求
  • 雙重檢查:數據庫唯一約束作為最後防線

總結

Token 令牌機制是防重復提交經典方案:生成→存儲→攜帶→驗證→銷毀 閉環。工具類封裝消除重複代碼,分佈式場景遷移至 Redis,配合前端防抖、數據庫約束構建多層防護體系。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved