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

ASP.NET 防止重複提交表單:三種核心策略完整實作

     編輯:ASP.NET 基礎

前言

使用者在提交表單後若按下 F5 重整、或點擊瀏覽器「上一頁」再送出,伺服器可能重複處理相同資料,造成重複訂單、重複發文、資料庫髒資料。本文介紹三種主流防重策略:PRG 模式(Post-Redirect-Get)、Token 模式(單次權杖)、快取控制模式,並提供完整 ASP.NET WebForms 實作範例。

策略比較總覽

策略 核心原理 優點 缺點 適用場景
PRG 模式 Post 成功後 Redirect 到 GET 頁面 簡單、標準、瀏覽器原生支援、無 Session 依賴 需額外頁面或同頁 Redirect、無法防止惡意重送 一般表單提交、發文、註冊
Token 模式 表單隱藏唯一 Token,Server 驗證後失效 嚴格防重、支援多視窗、可攔截惡意重送 需 Session/資料庫儲存 Token、稍複雜 金流、關鍵業務、API 介接
快取控制 Response 設定 No-Cache,瀏覽器不快取表單 零程式碼侵入、配合 PRG 效果佳 無法單獨防重、僅輔助 所有表單頁面通用設定

策略一:PRG 模式(最推薦基礎做法)

原理

  1. 使用者 POST 表單至 Post.aspx。
  2. 伺服器驗證、寫入資料庫成功。
  3. 伺服器回應 302 Redirect 導向 Post.aspx(或成功頁)。
  4. 瀏覽器以 GET 重新載入頁面,歷史記錄替換為 GET 請求。
  5. 使用者按 F5 只會重送 GET,不會再次 POST。

完整程式碼

public partial class Post : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        // 1. 禁用快取(輔助防重)
        Response.Cache.SetNoStore();
        Response.Cache.SetExpires(DateTime.MinValue);
        Response.Cache.SetCacheability(HttpCacheability.NoCache);

        // 2. 檢查 Session 標記(可選,雙重保險)
        if (Session["IsSubmit"] != null && (bool)Session["IsSubmit"])
        {
            Session["IsSubmit"] = false; // 重置
            lblMsg.Text = " * 提交成功!";
        }
        else
        {
            lblMsg.Text = "";
        }
    }

    protected void btnOK_Click(object sender, EventArgs e)
    {
        if (string.IsNullOrWhiteSpace(txtTitle.Text))
        {
            lblMsg.Text = " * 標題不可空白!";
            return;
        }
        if (string.IsNullOrWhiteSpace(txtContent.Text))
        {
            lblMsg.Text = " * 內容不可空白!";
            return;
        }

        try
        {
            // 3. 執行資料庫寫入
            SaveToDatabase(txtTitle.Text, txtContent.Text);

            // 4. 設定成功旗標(給 Redirect 後的 GET 請求看)
            Session["IsSubmit"] = true;

            // 5. 關鍵:Redirect 到自己(PRG)
            Response.Redirect(Request.RawUrl); // 或 "Post.aspx"
        }
        catch (Exception ex)
        {
            lblMsg.Text = " * 系統錯誤,請稍後再試。";
            LogError(ex);
        }
    }

    private void SaveToDatabase(string title, string content)
    {
        // 實際 ADO.NET / EF Core 寫入邏輯
    }
}

策略二:Token 單次權杖模式(最嚴格)

原理

  1. 頁面首次載入(GET)時,產生一組隨機 Token(Guid),存入 Session 並寫入表單 <input type="hidden" name="__TOKEN" />。
  2. 使用者提交(POST)時,攜帶 Token。
  3. 伺服器比對 Session 中的 Token:
    • 相符 → 處理業務、立即移除/更新 Session Token、回應成功。
    • 不符或 Session 無 Token → 視為重複提交或偽造,拒絕處理。

完整程式碼

public partial class SecurePost : System.Web.UI.Page
{
    private const string TokenKey = "FormToken_Post";

    protected void Page_Load(object sender, EventArgs e)
    {
        Response.Cache.SetNoStore();

        if (!IsPostBack)
        {
            // GET 請求:產生新 Token
            GenerateNewToken();
        }
        else
        {
            // POST 請求:驗證 Token
            ValidateAndProcessToken();
        }
    }

    private void GenerateNewToken()
    {
        string token = Guid.NewGuid().ToString("N");
        Session[TokenKey] = token;
        hdnToken.Value = token; // HiddenField 控制項
    }

    private void ValidateAndProcessToken()
    {
        string submittedToken = hdnToken.Value;
        string sessionToken = Session[TokenKey] as string;

        // 移除舊 Token,確保一次性
        Session.Remove(TokenKey);

        if (string.IsNullOrEmpty(submittedToken) || 
            submittedToken != sessionToken)
        {
            lblMsg.Text = " * 無效或重複的提交請求。";
            GenerateNewToken(); // 給使用者重新嘗試的新 Token
            return;
        }

        // Token 驗證通過,執行業務
        if (string.IsNullOrWhiteSpace(txtTitle.Text) || 
            string.IsNullOrWhiteSpace(txtContent.Text))
        {
            lblMsg.Text = " * 資料不完整。";
            GenerateNewToken();
            return;
        }

        try
        {
            SaveToDatabase(txtTitle.Text, txtContent.Text);
            lblMsg.Text = " * 提交成功!";
            // 成功後不再產生新 Token,避免重送;或導向成功頁
        }
        catch (Exception ex)
        {
            lblMsg.Text = " * 系統錯誤。";
            LogError(ex);
            GenerateNewToken(); // 失敗給予重試機會
        }
    }
}

.aspx 表單加入 HiddenField

<asp:HiddenField ID="hdnToken" runat="server" />

策略三:快取控制(全域套用)

在 web.config 或 BasePage 統一設定,防止瀏覽器快取表單頁面,配合 PRG 使用效果最佳。

web.config 系統級設定

<system.webServer>
  <httpProtocol>
    <customHeaders>
      <add name="Cache-Control" value="no-store, no-cache, must-revalidate" />
      <add name="Pragma" value="no-cache" />
      <add name="Expires" value="0" />
    </customHeaders>
  </httpProtocol>
</system.webServer>

BasePage 程式碼級設定

public class BasePage : System.Web.UI.Page
{
    protected override void OnInit(EventArgs e)
    {
        Response.Cache.SetNoStore();
        Response.Cache.SetExpires(DateTime.MinValue);
        Response.Cache.SetCacheability(HttpCacheability.NoCache);
        Response.Cache.SetValidUntilExpires(false);
        base.OnInit(e);
    }
}

進階:AJAX 表單防重

若使用 jQuery AJAX 提交,需在前端鎖定按鈕、後端同樣驗證 Token:

// 前端
$("#btnSubmit").prop("disabled", true).text("處理中...");
$.post("api/submit", { token: $("#hdnToken").val(), ... })
  .always(() => $("#btnSubmit").prop("disabled", false).text("送出"));

// 後端 API Controller
[HttpPost]
public IActionResult Submit([FromForm] SubmitModel model)
{
    if (!ValidateToken(model.Token)) return BadRequest("Token 無效");
    // 處理...
    return Ok();
}

常見誤區與避坑指南

  1. 只用 `Response.Cache.SetNoStore()` 不做 Redirect:瀏覽器仍會在歷史記錄保留 POST,按「上一頁」再送出依然會重複提交。
  2. Session["IsSubmit"] 未重置:導致正常 GET 請求也顯示「成功訊息」。
  3. Token 驗證失敗後未發新 Token:使用者無法重試,體驗極差。
  4. 多標籤頁操作:PRG 模式下,標籤 A 送出後 Redirect,標籤 B 仍保留舊表單,送出會被視為重複。Token 模式天然支援多標籤(各自有獨立 Token)。
  5. 分散式環境 Session 不共享:Token 模式需改用 Redis / SQL Server Session,否則負載平衡導致 Token 驗證失效。

總結建議

業務層級 推薦組合
一般表單(發文、聯絡我們) PRG + 快取控制 + 簡易 Session Flag
重要業務(會員註冊、修改密碼) PRG + Token 模式
金流/付款/核心交易 Token 模式 + 冪等性設計 + 分散式鎖
AJAX API 前端鎖定按鈕 + 後端 Token/冪等 Key 驗證

核心原則:伺服器端必須具備「識別並拒絕重複請求」的能力,單靠前端攔截(如 disable 按鈕)完全不可靠。PRG 是最低門檻,Token 是進階標準,冪等性設計才是終極解法。

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