為何要封裝 Cookie 集合?
- 單一 Cookie 存放多個鍵值 (
UserId=123&Role=Admin&Theme=Dark),減少 Cookie 數量與請求標頭大小。
- 集中管理過期時間、加密、HttpOnly、Secure、SameSite 設定。
- 提供強型別屬性 (
CurrentUser.Id),避免魔術字串散落各處。
- 統一處理 null、序列化、編碼細節。
完整封裝類別
using System;
using System.Web;
public static class AuthCookie
{
private const string CookieName = "AuthCookie";
private static HttpResponse Response => HttpContext.Current.Response;
private static HttpRequest Request => HttpContext.Current.Request;
// 內部存取器:自動建立/取得 HttpCookie 實例
private static HttpCookie Cookie
{
get => Request.Cookies[CookieName];
set
{
if (value == null) return;
// 移除舊 Cookie 避免重複
if (Request.Cookies[CookieName] != null)
Request.Cookies.Remove(CookieName);
Response.Cookies.Add(value);
}
}
// 新建空白 Cookie (預設設定)
private static HttpCookie NewCookie()
{
var c = new HttpCookie(CookieName)
{
HttpOnly = true,
Secure = HttpContext.Current.Request.IsSecureConnection, // 僅 HTTPS 傳送
SameSite = SameSiteMode.Lax,
Expires = DateTime.Now.AddDays(30)
};
return c;
}
// 公開屬性:登入狀態
public static bool IsLoggedIn
{
get => Cookie != null && bool.TryParse(Cookie.Values["IsLoggedIn"], out var b) && b;
set
{
var c = Cookie ?? NewCookie();
c.Values["IsLoggedIn"] = value.ToString();
Cookie = c;
}
}
// 公開屬性:使用者 ID (int)
public static int UserId
{
get => Cookie != null && int.TryParse(Cookie.Values["UserId"], out var id) ? id : 0;
set
{
var c = Cookie ?? NewCookie();
c.Values["UserId"] = value.ToString();
Cookie = c;
}
}
// 公開屬性:角色清單 (逗號分隔)
public static string Roles
{
get => Cookie?.Values["Roles"] ?? string.Empty;
set
{
var c = Cookie ?? NewCookie();
c.Values["Roles"] = value;
Cookie = c;
}
}
// 公開屬性:偏好主題
public static string Theme
{
get => Cookie?.Values["Theme"] ?? "Light";
set
{
var c = Cookie ?? NewCookie();
c.Values["Theme"] = value;
Cookie = c;
}
}
// 登出:移除 Cookie
public static void SignOut()
{
if (Cookie != null)
{
// 設為過期即刪除
Cookie.Expires = DateTime.Now.AddDays(-1);
Response.Cookies.Set(Cookie);
}
}
// 更新過期時間 (滑動過期)
public static void Renew()
{
if (Cookie != null)
{
Cookie.Expires = DateTime.Now.AddDays(30);
Response.Cookies.Set(Cookie);
}
}
}
使用範例
登入成功時
protected void btnLogin_Click(object sender, EventArgs e)
{
var user = UserService.Validate(txtUser.Text, txtPwd.Text);
if (user != null)
{
AuthCookie.IsLoggedIn = true;
AuthCookie.UserId = user.Id;
AuthCookie.Roles = string.Join(",", user.Roles);
AuthCookie.Theme = user.PreferredTheme;
Response.Redirect("~/Dashboard.aspx");
}
else
{
lblError.Text = "帳號密碼錯誤";
}
}
頁面驗證登入
protected void Page_Load(object sender, EventArgs e)
{
if (!AuthCookie.IsLoggedIn)
{
Response.Redirect("~/Login.aspx?ReturnUrl=" + Server.UrlEncode(Request.RawUrl));
}
else
{
// 取得強型別值
int uid = AuthCookie.UserId;
bool isAdmin = AuthCookie.Roles.Contains("Admin");
litWelcome.Text = $"歡迎,使用者 {uid}";
}
}
登出
protected void btnLogout_Click(object sender, EventArgs e)
{
AuthCookie.SignOut();
Response.Redirect("~/Login.aspx");
}
進階:加密敏感欄位
// 簡易 AES 加密 (生產請用 DataProtectionProvider / ASP.NET Core DataProtection)
private static string Encrypt(string plain) => Convert.ToBase64String(ProtectedData.Protect(Encoding.UTF8.GetBytes(plain), null, DataProtectionScope.CurrentUser));
private static string Decrypt(string cipher) => Encoding.UTF8.GetString(ProtectedData.Unprotect(Convert.FromBase64String(cipher), null, DataProtectionScope.CurrentUser));
// 屬性存取時自動加解密
public static string SensitiveToken
{
get => Cookie != null ? Decrypt(Cookie.Values["Token"]) : null;
set
{
var c = Cookie ?? NewCookie();
c.Values["Token"] = Encrypt(value);
Cookie = c;
}
}
安全性檢查清單
| 項目 |
建議設定 |
| HttpOnly |
true (防 XSS 竊取) |
| Secure |
true (僅 HTTPS 傳送,生產環境強制) |
| SameSite |
Lax (平衡 CSRF 防護與跨站導向) / Strict (嚴格) |
| Domain |
明確設定 (如 .example.com) 而非預設 |
| Path |
/ (全站) 或特定應用程式路徑 |
| Expires |
滑動過期 (每次請求 Renew) 或固定 30 天 |
常見問題
- Request.Cookies.Remove 後仍存在:只移除集合參考,必須設定
Expires = 過去日期 並 Response.Cookies.Add/Set 才能真正刪除瀏覽器端 Cookie。
- 子網域共享:
Domain = ".example.com" 並確保所有子站應用程式金鑰 machineKey 一致 (加密/驗證用)。
- 4KB 限制:單一 Cookie 總大小 (含名稱、值、屬性) 不超過 4096 bytes;集合寫法可減少 Cookie 數量但總容量不變。
- 編碼問題:值含分號、逗號、等號時會干擾解析,建議
HttpUtility.UrlEncode 儲存、UrlDecode 讀取。