在每個頁面的 `Page_Load` 重複撰寫 `if (Session["User"] == null) Response.Redirect("Login.aspx")` 不僅違反 DRY 原則,還易漏寫導致安全漏洞。本文比較三種集中化方案優劣。
public class BasePage : System.Web.UI.Page
{
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
// 排除登入頁、公開頁面
string[] publicPages = { "/Login.aspx", "/Register.aspx", "/Public/" };
string rawUrl = Request.RawUrl.ToLower();
if (!publicPages.Any(p => rawUrl.StartsWith(p)))
{
if (Session["UserID"] == null)
{
// AJAX 請求回傳 JSON,一般請求導向登入頁
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
{
Response.StatusCode = 401;
Response.ContentType = "application/json";
Response.Write("{\"error\":\"session_expired\"}");
Response.End();
}
else
{
Response.Redirect($"~/Login.aspx?ReturnUrl={Server.UrlEncode(Request.RawUrl)}");
}
}
}
}
}
// 頁面繼承 BasePage 而非 Page
public partial class Admin_Default : BasePage { ... }
| 優點 | 缺點 |
|---|---|
| 彈性高:可逐頁決定是否繼承 | 現有專案需逐頁修改繼承關係 |
| 邏輯集中,易維護 | 無法攔截非 .aspx 請求(如 .ashx、.asmx、靜態檔) |
| 可存取完整 Page 生命週期事件 | |
| 支援 AJAX/一般請求差異化處理 |
public class SessionCheckModule : IHttpModule
{
public void Init(HttpApplication context)
{
// AcquireRequestState 之後 Session 才可用
context.AcquireRequestState += OnAcquireRequestState;
}
private void OnAcquireRequestState(object sender, EventArgs e)
{
var app = (HttpApplication)sender;
var ctx = app.Context;
// 排除公開路徑
if (IsPublicPath(ctx.Request.Path)) return;
// 檢查 Session
if (ctx.Session["UserID"] == null)
{
if (IsAjaxRequest(ctx.Request))
{
ctx.Response.StatusCode = 401;
ctx.Response.ContentType = "application/json";
ctx.Response.Write("{\"error\":\"session_expired\"}");
}
else
{
ctx.Response.Redirect($"~/Login.aspx?ReturnUrl={ctx.Server.UrlEncode(ctx.Request.RawUrl)}");
}
ctx.Response.End(); // 短路管線
}
}
private bool IsPublicPath(string path)
=> path.StartsWith("/Login.aspx", StringComparison.OrdinalIgnoreCase)
|| path.StartsWith("/Public/", StringComparison.OrdinalIgnoreCase)
|| path.EndsWith(".axd", StringComparison.OrdinalIgnoreCase); // 排除 WebResource.axd 等
private bool IsAjaxRequest(HttpRequest req)
=> req.Headers["X-Requested-With"] == "XMLHttpRequest";
public void Dispose() { }
}
<system.webServer>
<modules>
<add name="SessionCheckModule" type="MyNamespace.SessionCheckModule, MyAssembly" preCondition="managedHandler" />
</modules>
</system.webServer>
| 優點 | 缺點 |
|---|---|
| 一次配置,全站生效(含 .ashx、.asmx、Controller) | 較早攔截,Session 可能尚未建立(需在 AcquireRequestState 之後) |
| 效能最佳:在管線早期短路,不執行後續 Handler | 邏輯較隱蔽,新人難發現 |
| 可攔截所有 managed 請求 | 靜態檔案 (.jpg, .css) 預設不經 managed 管線,除非設定 runAllManagedModulesForAllRequests |
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack && Session["UserID"] == null)
Response.Redirect("Login.aspx");
}
缺點明顯:程式碼冗餘、易漏寫、難維護、無法處理 AJAX、違反關注點分離。
| 維度 | BasePage | HttpModule | Page_Load |
|---|---|---|---|
| 覆蓋範圍 | .aspx 頁面 | 所有 managed 請求 | 單一頁面 |
| 遷移成本 | 中(逐頁改繼承) | 低(僅註冊模組) | 高(每頁加碼) |
| 彈性 | 高(可選擇性繼承) | 中(靠路徑排除) | 低 |
| 效能 | 良 | 最佳(早期短路) | 差 |
| 建議場景 | WebForms 專案、需頁面級控制 | 新專案、MVC 混合、追求徹底 | 極小專案、快速原型 |
在 `Global.asax` 或 `BasePage.OnInit` 中:
// 每次請求重置 Session 過期計時(滑動過期)
Session.Timeout = 20; // 分鐘,覆蓋 web.config 設定
新專案首選 HttpModule,徹底解決全站驗證;既有 WebForms 專案 採用 BasePage 平滑遷移;拒絕在 Page_Load 重複判斷。配合 AJAX 差異化回應,提升使用者體驗與安全性。