身份驗證與授權
驗證 vs 授權
- 身份驗證 Authentication:你是誰?(像門口的保全確認你的身份證)
- 授權 Authorization:你能做什麼?(像VIP 識別決定你能進哪些區域)
用戶請求 → 身份驗證(你是誰?)→ 授權(你能做什麼?)→ 存取資源
「請出示證件」 「確認你有 VIP 資格」
Cookie 驗證
// Program.cs - 設定 Cookie 驗證
builder.Services.AddAuthentication(
CookieAuthenticationDefaults.AuthenticationScheme) // 使用 Cookie 方案
.AddCookie(options =>
{
options.LoginPath = "/Account/Login"; // 未登入時導向登入頁
options.AccessDeniedPath = "/Account/Denied"; // 權限不足時導向
options.ExpireTimeSpan = TimeSpan.FromHours(2); // Cookie 2 小時過期
});
// AccountController.cs - 登入邏輯
public class AccountController : Controller
{
[HttpPost]
public async Task<IActionResult> Login(LoginViewModel model)
{
// 驗證帳號密碼(這裡簡化示範)
if (model.Username == "admin" && model.Password == "pass123")
{
// 建立 Claims(聲明)
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, model.Username), // 使用者名稱
new Claim(ClaimTypes.Role, "Admin"), // 角色
new Claim("Department", "IT") // 自訂 Claim
};
// 建立身份識別
var identity = new ClaimsIdentity(
claims,
CookieAuthenticationDefaults.AuthenticationScheme); // 驗證方案
// 登入(寫入 Cookie)
await HttpContext.SignInAsync(
new ClaimsPrincipal(identity)); // 建立主體並登入
return RedirectToAction("Index", "Home"); // 導向首頁
}
ModelState.AddModelError("", "帳號或密碼錯誤"); // 驗證失敗
return View(model); // 回到登入頁
}
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(); // 登出(清除 Cookie)
return RedirectToAction("Index", "Home"); // 導向首頁
}
}
JWT Token 驗證
// Program.cs - 設定 JWT 驗證
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true, // 驗證發行者
ValidateAudience = true, // 驗證受眾
ValidateLifetime = true, // 驗證有效期
ValidateIssuerSigningKey = true, // 驗證簽章金鑰
ValidIssuer = builder.Configuration["Jwt:Issuer"], // 合法發行者
ValidAudience = builder.Configuration["Jwt:Audience"], // 合法受眾
IssuerSigningKey = new SymmetricSecurityKey( // 簽章金鑰
Encoding.UTF8.GetBytes(
builder.Configuration["Jwt:Key"]!)) // 從設定檔讀取
};
});
// 產生 JWT Token
public string GenerateToken(User user)
{
var claims = new[]
{
new Claim(ClaimTypes.Name, user.Username), // 使用者名稱
new Claim(ClaimTypes.Role, user.Role), // 角色
new Claim(JwtRegisteredClaimNames.Jti,
Guid.NewGuid().ToString()) // Token 唯一識別碼
};
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config["Jwt:Key"]!)); // 金鑰
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"], // 發行者
audience: _config["Jwt:Audience"], // 受眾
claims: claims, // 聲明
expires: DateTime.Now.AddHours(1), // 1 小時後過期
signingCredentials: new SigningCredentials(
key, SecurityAlgorithms.HmacSha256)); // 簽章演算法
return new JwtSecurityTokenHandler().WriteToken(token); // 產生 Token 字串
}
[Authorize] 與 [AllowAnonymous]
// 需要登入才能存取的 Controller
[Authorize] // 整個 Controller 需要登入
public class DashboardController : Controller
{
public IActionResult Index()
{
var name = User.Identity?.Name; // 取得登入者名稱
return View(); // 回傳儀表板頁面
}
[AllowAnonymous] // 這個 Action 允許匿名存取
public IActionResult PublicPage()
{
return View(); // 不用登入也能看
}
[Authorize(Roles = "Admin")] // 只有 Admin 角色可以存取
public IActionResult AdminOnly()
{
return View(); // 管理員專用
}
[Authorize(Policy = "AtLeast18")] // 自訂授權策略
public IActionResult AdultContent()
{
return View(); // 符合策略才能存取
}
}
// Program.cs - 設定授權策略
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AtLeast18", policy =>
policy.RequireClaim("Age") // 需要 Age Claim
.RequireAssertion(ctx =>
{
var age = int.Parse(
ctx.User.FindFirst("Age")?.Value ?? "0"); // 取得年齡
return age >= 18; // 年滿 18 歲
}));
});
ASP.NET Identity 基礎
// Program.cs - 設定 ASP.NET Identity
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
{
options.Password.RequireDigit = true; // 密碼要有數字
options.Password.RequiredLength = 8; // 密碼至少 8 碼
options.Password.RequireUppercase = true; // 密碼要有大寫字母
options.Lockout.MaxFailedAccessAttempts = 5; // 失敗 5 次鎖定
options.Lockout.DefaultLockoutTimeSpan =
TimeSpan.FromMinutes(15); // 鎖定 15 分鐘
})
.AddRoles<IdentityRole>() // 啟用角色管理
.AddEntityFrameworkStores<AppDbContext>(); // 使用 EF Core 儲存
🤔 我這樣寫為什麼會錯?
❌ 錯誤 1:把密碼存成明文
// ❌ 明文儲存密碼(超級危險!)
var user = new User
{
Username = "admin",
Password = "mypassword123" // ❌ 明文!資料庫被偷密碼就外洩了
};
db.Users.Add(user);
// ✅ 使用雜湊(Hash)
var passwordHasher = new PasswordHasher<User>(); // 建立密碼雜湊器
var user = new User { Username = "admin" }; // 建立使用者
user.PasswordHash = passwordHasher.HashPassword(
user, "mypassword123"); // 雜湊後儲存
db.Users.Add(user); // 存入資料庫
// 驗證時
var result = passwordHasher.VerifyHashedPassword(
user, user.PasswordHash, inputPassword); // 比對雜湊值
為什麼? 資料庫被入侵時,明文密碼會直接曝光。雜湊是單向的,即使被偷也無法還原。
❌ 錯誤 2:JWT 金鑰寫在程式碼裡
// ❌ 金鑰寫死在程式碼中(推上 Git 就洩漏了)
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("my-super-secret-key-12345")); // ❌ 硬編碼金鑰
// ✅ 從設定檔或環境變數讀取
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(
builder.Configuration["Jwt:Key"]!)); // ✅ 從設定檔讀取
// appsettings.json(開發環境用)
{
"Jwt": {
"Key": "development-only-key-do-not-use-in-prod"
}
}
// 正式環境用環境變數或 Azure Key Vault
為什麼? 金鑰寫在程式碼裡,推到 Git 就全世界都看得到。應該用設定檔或環境變數管理機密資訊。
❌ 錯誤 3:沒有驗證 JWT Token 的有效期
// ❌ 停用有效期驗證(永遠不過期的 Token)
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateLifetime = false, // ❌ 不檢查過期!Token 被偷就永遠能用
};
// ✅ 啟用所有驗證
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateLifetime = true, // ✅ 檢查有效期
ClockSkew = TimeSpan.FromMinutes(5), // 允許 5 分鐘時鐘偏差
ValidateIssuer = true, // 驗證發行者
ValidateAudience = true, // 驗證受眾
ValidateIssuerSigningKey = true, // 驗證簽章
};
為什麼? 停用有效期驗證等於 Token 永遠有效,一旦被竊取,攻擊者可以永遠冒充該用戶。