使用者在提交表單後若按下 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 效果佳 | 無法單獨防重、僅輔助 | 所有表單頁面通用設定 |
Post.aspx。302 Redirect 導向 Post.aspx(或成功頁)。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 寫入邏輯
}
}
<input type="hidden" name="__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(); // 失敗給予重試機會
}
}
}
<asp:HiddenField ID="hdnToken" runat="server" />
在 web.config 或 BasePage 統一設定,防止瀏覽器快取表單頁面,配合 PRG 使用效果最佳。
<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>
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);
}
}
若使用 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();
}
| 業務層級 | 推薦組合 |
|---|---|
| 一般表單(發文、聯絡我們) | PRG + 快取控制 + 簡易 Session Flag |
| 重要業務(會員註冊、修改密碼) | PRG + Token 模式 |
| 金流/付款/核心交易 | Token 模式 + 冪等性設計 + 分散式鎖 |
| AJAX API | 前端鎖定按鈕 + 後端 Token/冪等 Key 驗證 |
核心原則:伺服器端必須具備「識別並拒絕重複請求」的能力,單靠前端攔截(如 disable 按鈕)完全不可靠。PRG 是最低門檻,Token 是進階標準,冪等性設計才是終極解法。