身份驗證機制
什麼是身份驗證(Authentication)?
身份驗證就是確認「你是誰」的過程。
💡 比喻:進公司大門
- Authentication(驗證) = 你刷員工證進大門(證明你是員工)
- Authorization(授權) = 你能進哪些房間(你有什麼權限)
- 先驗證身份,再決定權限
Session-based vs Token-based 驗證
Session-based(傳統方式)
登入流程:
瀏覽器 伺服器
|--- POST /login -->|
| {帳號, 密碼} |
| | 驗證成功!
| | 建立 Session(存在伺服器記憶體)
|<-- Set-Cookie: sessionId=abc123 -|
| |
|--- GET /profile -->|
| Cookie: sessionId=abc123 |
| | 用 sessionId 查找 Session
|<-- {使用者資料} --|
Session 存在伺服器端(記憶體或 Redis)
Token-based(JWT 方式)
登入流程:
瀏覽器 伺服器
|--- POST /login -->|
| {帳號, 密碼} |
| | 驗證成功!
| | 產生 JWT Token
|<-- { token: "eyJhbG..." } --|
| |
|--- GET /profile -->|
| Authorization: Bearer eyJhbG..|
| | 驗證 Token 簽章
|<-- {使用者資料} --|
Token 存在客戶端(不需要伺服器存 Session)
兩種方式比較
特性 Session-based Token-based (JWT)
──────────────────────────────────────────────────────
狀態 有狀態(伺服器存) 無狀態(客戶端存)
擴展性 難(多伺服器要共享) 易(每台都能驗證)
儲存位置 伺服器記憶體/Redis 客戶端 Cookie/Storage
跨域 困難 容易(API 友好)
登出 刪除 Session 即可 需要額外機制(黑名單)
適用場景 傳統網站 API、SPA、手機 App
JWT 結構解析
JWT 的三個部分
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.abc123signature
|---- Header ----|.|---- Payload ----|.|--- Signature ---|
用「.」分隔三個部分,每個部分都是 Base64 編碼
Header(標頭)
{
"alg": "HS256",
"typ": "JWT"
}
Payload(酬載)
{
"sub": "1234",
"name": "小明",
"role": "admin",
"exp": 1700000000,
"iat": 1699900000
}
常見 Claims(聲明):
├── sub → Subject(使用者 ID)
├── name → 使用者名稱
├── role → 角色/權限
├── exp → Expiration(過期時間,Unix 時間戳)
├── iat → Issued At(簽發時間)
└── iss → Issuer(簽發者)
Signature(簽章)
簽章 = HMACSHA256(
base64(header) + "." + base64(payload),
secret_key
)
簽章的用途:
├── 確保 Token 沒有被竄改
├── 只有擁有 secret_key 的伺服器才能產生有效簽章
└── 注意:Payload 只是 Base64 編碼,不是加密!任何人都能解碼看到內容
C# 產生 JWT
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
// 定義密鑰(至少 32 字元)
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("your-super-secret-key-at-least-32-chars!")
);
// 建立簽章憑證
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
// 定義 Token 中的 Claims(使用者資訊)
var claims = new[]
{
// 使用者 ID
new Claim(ClaimTypes.NameIdentifier, "1234"),
// 使用者名稱
new Claim(ClaimTypes.Name, "小明"),
// 使用者角色
new Claim(ClaimTypes.Role, "Admin"),
};
// 建立 JWT Token
var token = new JwtSecurityToken(
issuer: "my-app", // 簽發者
audience: "my-app-users", // 對象
claims: claims, // 使用者資訊
expires: DateTime.UtcNow.AddHours(1), // 1 小時後過期
signingCredentials: credentials // 簽章
);
// 序列化成字串
var tokenString = new JwtSecurityTokenHandler().WriteToken(token);
// 輸出類似:eyJhbGciOiJIUzI1NiIs...
Console.WriteLine(tokenString);
OAuth 2.0 流程
💡 比喻:LINE Login 你去一個新網站,它說「用 LINE 登入」:
- 網站把你「帶到 LINE 的登入頁面」
- 你在 LINE 輸入帳號密碼(不是在那個網站!)
- LINE 問你:「這個網站想要你的名字和大頭貼,可以嗎?」
- 你按「同意」
- LINE 給那個網站一個「通行證」(Access Token)
- 網站用通行證去 LINE 拿你的名字和大頭貼
OAuth 2.0 授權碼流程
使用者 你的網站 LINE(授權伺服器)
| | |
|-- 點擊「LINE 登入」-->| |
| |--- 302 重導向 ------->|
| | 到 LINE 登入頁 |
|<------------ LINE 登入頁面 -----------|
|-- 輸入帳密、同意 -------------------->|
| | |
| |<-- 302 帶 auth code --|
| | |
| |--- POST 用 code ----->|
| | 換 access token |
| |<-- access token ------|
| | |
| |--- GET /userinfo ---->|
| | 帶 access token |
| |<-- 使用者資料 --------|
|<-- 登入成功 ---| |
Refresh Token 機制
為什麼需要 Refresh Token?
Access Token 壽命短(例如 15 分鐘),過期了怎麼辦?
不能每次都要使用者重新登入吧!
解決方案:
├── Access Token → 短期(15 分鐘),用來存取 API
└── Refresh Token → 長期(7 天),用來換新的 Access Token
流程:
1. 登入 → 拿到 Access Token + Refresh Token
2. 用 Access Token 存取 API
3. Access Token 過期了
4. 用 Refresh Token 換一組新的 Access Token + Refresh Token
5. 繼續存取 API
// 模擬 Refresh Token 流程
public class TokenService
{
// 換發新的 Token 組
public async Task<TokenResponse> RefreshTokenAsync(string refreshToken)
{
// 驗證 Refresh Token 是否有效
var storedToken = await _db.RefreshTokens
.FirstOrDefaultAsync(t => t.Token == refreshToken);
// 檢查 Token 是否存在且未過期
if (storedToken == null || storedToken.ExpiresAt < DateTime.UtcNow)
{
// Refresh Token 無效或已過期,需要重新登入
throw new UnauthorizedAccessException("Refresh Token 已過期,請重新登入");
}
// 產生新的 Access Token
var newAccessToken = GenerateJwtToken(storedToken.UserId);
// 產生新的 Refresh Token(舊的作廢)
var newRefreshToken = GenerateRefreshToken();
// 把舊的 Refresh Token 標記為已使用
storedToken.IsRevoked = true;
// 儲存新的 Refresh Token
await _db.RefreshTokens.AddAsync(new RefreshToken
{
Token = newRefreshToken,
UserId = storedToken.UserId,
ExpiresAt = DateTime.UtcNow.AddDays(7)
});
await _db.SaveChangesAsync();
// 回傳新的 Token 組
return new TokenResponse
{
AccessToken = newAccessToken,
RefreshToken = newRefreshToken
};
}
}
🤔 我這樣寫為什麼會錯?
❌ 錯誤 1:把 JWT 存在 localStorage
// ❌ 錯誤:localStorage 容易被 XSS 攻擊竊取
localStorage.setItem("token", jwtToken);
// 如果網站有 XSS 漏洞,駭客可以用 JavaScript 讀取你的 Token
// ✅ 正確:存在 HttpOnly Cookie 中
// HttpOnly Cookie 無法被 JavaScript 讀取,更安全
// 在伺服器端設定:
// Set-Cookie: token=eyJhbG...; HttpOnly; Secure; SameSite=Strict
// ✅ ASP.NET Core 設定 HttpOnly Cookie
Response.Cookies.Append("token", jwtToken, new CookieOptions
{
HttpOnly = true, // JavaScript 無法讀取
Secure = true, // 只在 HTTPS 傳送
SameSite = SameSiteMode.Strict, // 防止 CSRF
Expires = DateTimeOffset.UtcNow.AddHours(1) // 1 小時後過期
});
❌ 錯誤 2:沒有驗證 JWT 簽章
// ❌ 錯誤:只解碼 Token,沒有驗證簽章
var handler = new JwtSecurityTokenHandler();
// 這只是把 Base64 解碼,任何人都可以偽造一個 Token!
var token = handler.ReadJwtToken(tokenString);
var userId = token.Claims.First(c => c.Type == "sub").Value;
// ✅ 正確:用 ValidateToken 驗證簽章
var validationParams = new TokenValidationParameters
{
// 驗證簽發者
ValidateIssuer = true,
ValidIssuer = "my-app",
// 驗證對象
ValidateAudience = true,
ValidAudience = "my-app-users",
// 驗證簽章(最重要!)
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("your-super-secret-key-at-least-32-chars!")
),
// 驗證過期時間
ValidateLifetime = true,
};
// ValidateToken 會驗證簽章、過期時間等
var principal = handler.ValidateToken(tokenString, validationParams, out _);
❌ 錯誤 3:JWT 放敏感資料
❌ 錯誤:把密碼或信用卡放在 JWT Payload 中
{
"sub": "1234",
"password": "mypassword123", ← 任何人都能看到!
"creditCard": "4111-1111-1111-1111" ← 超危險!
}
JWT 的 Payload 只是 Base64 編碼,不是加密!
任何人拿到 Token 都可以解碼看到所有內容。
✅ 正確:只放不敏感的識別資訊
{
"sub": "1234",
"name": "小明",
"role": "user"
}
💡 重點整理
| 概念 | 說明 |
|---|---|
| Authentication | 驗證身份(你是誰) |
| Authorization | 授權(你能做什麼) |
| Session-based | 伺服器端存狀態,用 Cookie 傳 Session ID |
| JWT | 無狀態的 Token,包含 Header.Payload.Signature |
| OAuth 2.0 | 第三方登入的標準協議(LINE、Google 登入) |
| Refresh Token | 用來換發新 Access Token,避免頻繁重新登入 |
| HttpOnly Cookie | 最安全的 Token 儲存方式 |