承接 Forms 驗證系列,本文提供完整實戰代碼:SQL Server 使用者表、驗證輔助類、登入頁邏輯、FormsAuthenticationTicket 建立與加密 Cookie 發送、登出與逾期處理。
CREATE TABLE Users (
UserId INT IDENTITY PRIMARY KEY,
UserName NVARCHAR(50) NOT NULL UNIQUE,
PasswordHash VARBINARY(64) NOT NULL, -- 儲存 SHA256 雜湊
PasswordSalt VARBINARY(16) NOT NULL,
Role NVARCHAR(50) NOT NULL,
IsActive BIT DEFAULT 1
);
public static class AuthHelper
{
private static string ConnStr = ConfigurationManager.ConnectionStrings["AuthDb"].ConnectionString;
public static bool ValidateUser(string username, string password, out UserInfo user)
{
user = null;
using (var conn = new SqlConnection(ConnStr))
using (var cmd = new SqlCommand("SELECT UserId, UserName, PasswordHash, PasswordSalt, Role FROM Users WHERE UserName=@u AND IsActive=1", conn))
{
cmd.Parameters.AddWithValue("@u", username);
conn.Open();
using (var rd = cmd.ExecuteReader())
{
if (rd.Read())
{
byte[] hash = (byte[])rd["PasswordHash"];
byte[] salt = (byte[])rd["PasswordSalt"];
byte[] inputHash = HashPassword(password, salt);
if (StructuralComparisons.StructuralEqualityComparer.Equals(hash, inputHash))
{
user = new UserInfo
{
UserId = rd.GetInt32(0),
UserName = rd.GetString(1),
Role = rd.GetString(4)
};
return true;
}
}
}
}
return false;
}
private static byte[] HashPassword(string pwd, byte[] salt)
{
using (var sha = SHA256.Create())
{
byte[] pwdBytes = Encoding.UTF8.GetBytes(pwd);
byte[] combined = new byte[salt.Length + pwdBytes.Length];
Buffer.BlockCopy(salt, 0, combined, 0, salt.Length);
Buffer.BlockCopy(pwdBytes, 0, combined, salt.Length, pwdBytes.Length);
return sha.ComputeHash(combined);
}
}
}
public class UserInfo { public int UserId { get; set; } public string UserName { get; set; } public string Role { get; set; } }
protected void btnLogin_Click(object sender, EventArgs e)
{
if (AuthHelper.ValidateUser(txtUserName.Text, txtPassword.Text, out UserInfo user))
{
// 建立 Ticket:UserData 存放 UserId|Role
string userData = $"{user.UserId}|{user.Role}";
var ticket = new FormsAuthenticationTicket(
version: 1,
name: user.UserName,
issueDate: DateTime.Now,
expiration: DateTime.Now.AddMinutes(FormsAuthentication.Timeout.TotalMinutes),
isPersistent: chkRememberMe.Checked,
userData: userData,
cookiePath: FormsAuthentication.FormsCookiePath);
// 加密並寫入 Cookie
string encTicket = FormsAuthentication.Encrypt(ticket);
var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket)
{
HttpOnly = true,
Secure = FormsAuthentication.RequireSSL,
Expires = ticket.IsPersistent ? ticket.Expiration : DateTime.MinValue
};
Response.Cookies.Add(cookie);
// 導向原請求頁面或預設頁
string returnUrl = Request.QueryString["ReturnUrl"];
Response.Redirect(string.IsNullOrEmpty(returnUrl) ? "Default.aspx" : returnUrl);
}
else
{
lblError.Text = "帳號或密碼錯誤";
}
}
protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
if (HttpContext.Current.User?.Identity is FormsIdentity fid)
{
var ticket = fid.Ticket;
string[] parts = ticket.UserData.Split('|');
int userId = int.Parse(parts[0]);
string role = parts[1];
// 可選:從快取/資料庫載入完整權限
string[] permissions = PermissionService.GetPermissions(userId);
HttpContext.Current.User = new CustomPrincipal(fid, new[] { role }, userId, permissions);
Thread.CurrentPrincipal = HttpContext.Current.User;
}
}
// 登出按鈕
protected void btnLogout_Click(object sender, EventArgs e)
{
FormsAuthentication.SignOut(); // 清除 Cookie
Response.Redirect("Login.aspx");
}
// Web.config 設定逾期 (滑動逾期)
<forms loginUrl="Login.aspx" timeout="30" slidingExpiration="true" protection="All" />
RequireSSL="true" 強制 HTTPS 傳輸 Cookie。HttpOnly="true" 防止 XSS 竊取 Cookie。machineKey (驗證/加密金鑰)。