- 認證 vs 授權
- 認證方式總覽
- Session 認證
- Token 認證
- JWT 詳解
- OAuth 2.0
- OpenID Connect
- API 認證方式
- SSO 單一登入
- 最佳實踐
- 常見問題
- 總結
| 概念 |
英文 |
問題 |
比喻 |
| 認證 |
Authentication (AuthN) |
你是誰? |
出示身分證 |
| 授權 |
Authorization (AuthZ) |
你能做什麼? |
檢查門禁卡權限 |
認證(Authentication)
├── 驗證使用者身份
├── 「證明你是你」
└── 例:輸入帳號密碼登入
授權(Authorization)
├── 決定使用者能存取什麼資源
├── 「決定你能做什麼」
└── 例:管理員可以刪除文章,一般用戶不行
使用者請求
↓
1. 認證(Authentication)
「你是誰?請證明身份」
├── 成功 → 繼續
└── 失敗 → 401 Unauthorized
↓
2. 授權(Authorization)
「你有權限做這件事嗎?」
├── 有權限 → 執行操作
└── 無權限 → 403 Forbidden
| 狀態碼 |
名稱 |
意義 |
| 401 |
Unauthorized |
未認證(需要登入) |
| 403 |
Forbidden |
已認證但無權限 |
| 方式 |
適用場景 |
優點 |
缺點 |
| Session |
傳統 Web 應用 |
伺服器可控、可隨時撤銷 |
有狀態、難擴展 |
| JWT |
SPA、微服務、API |
無狀態、可擴展 |
無法即時撤銷 |
| OAuth 2.0 |
第三方登入、API 授權 |
標準化、安全 |
複雜度較高 |
| API Key |
伺服器對伺服器 |
簡單 |
安全性較低 |
| Basic Auth |
內部系統、測試 |
最簡單 |
不安全(需 HTTPS) |
你的應用類型是?
│
├─ 傳統 MVC Web 應用
│ └─ Session + Cookie
│
├─ SPA(React/Vue)+ API
│ └─ JWT 或 OAuth 2.0
│
├─ 微服務架構
│ └─ JWT + OAuth 2.0
│
├─ 行動 App
│ └─ OAuth 2.0 + JWT
│
├─ 第三方 API 整合
│ └─ OAuth 2.0
│
└─ 伺服器對伺服器
└─ API Key 或 OAuth 2.0 Client Credentials
1. 使用者登入
Client ──[帳號密碼]──→ Server
2. 伺服器建立 Session
Server: 產生 Session ID,儲存使用者資訊
Session Store: { "abc123": { userId: 1, name: "John" } }
3. 回傳 Session ID(透過 Cookie)
Server ──[Set-Cookie: sessionId=abc123]──→ Client
4. 後續請求自動帶 Cookie
Client ──[Cookie: sessionId=abc123]──→ Server
5. 伺服器驗證 Session
Server: 用 Session ID 查詢使用者資訊
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
httpOnly: true,
maxAge: 3600000,
sameSite: 'strict'
}
}));
app.post('/login', (req, res) => {
const { username, password } = req.body;
req.session.userId = user.id;
req.session.username = user.username;
res.json({ success: true });
});
app.post('/logout', (req, res) => {
req.session.destroy();
res.json({ success: true });
});
| 優點 |
缺點 |
| ✅ 伺服器完全控制 |
❌ 有狀態,需要儲存空間 |
| ✅ 可立即撤銷(登出) |
❌ 水平擴展需共享 Session |
| ✅ 資料存伺服器,安全 |
❌ CSRF 攻擊風險 |
| ✅ 實作相對簡單 |
❌ 不適合跨域/行動 App |
1. 使用者登入
Client ──[帳號密碼]──→ Server
2. 伺服器產生 Token
Server: 產生 Token(包含使用者資訊)
3. 回傳 Token
Server ──[Token: eyJhbGc...]──→ Client
4. Client 儲存 Token
存放位置:localStorage / sessionStorage / Cookie
5. 後續請求帶 Token
Client ──[Authorization: Bearer eyJhbGc...]──→ Server
6. 伺服器驗證 Token
Server: 驗證 Token 簽章,取出使用者資訊
| 比較項目 |
Session |
Token |
| 狀態 |
有狀態(Stateful) |
無狀態(Stateless) |
| 儲存位置 |
伺服器端 |
客戶端 |
| 擴展性 |
需共享 Session Store |
天生支援水平擴展 |
| 撤銷 |
立即撤銷 |
需額外機制 |
| 跨域 |
困難 |
簡單 |
| 適用場景 |
傳統 Web |
API、微服務、SPA |
JWT(JSON Web Token)是一種開放標準(RFC 7519),用於在各方之間安全地傳輸資訊。
JWT = Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. ← Header
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikp... ← Payload
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← Signature
{
"alg": "HS256",
"typ": "JWT"
}
{
"iss": "auth-server",
"sub": "1234567890",
"aud": "my-app",
"exp": 1735689600,
"iat": 1735686000,
"nbf": 1735686000,
"name": "John Doe",
"role": "admin"
}
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
Client 發送 JWT
↓
1. 解碼 Header 和 Payload(Base64)
↓
2. 使用相同演算法和密鑰重新計算簽章
↓
3. 比對簽章是否一致
├── 一致 → Token 未被竄改
└── 不一致 → Token 無效
↓
4. 檢查 exp(是否過期)
↓
5. 取出使用者資訊
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET;
function generateToken(user) {
return jwt.sign(
{
sub: user.id,
name: user.name,
role: user.role
},
SECRET,
{
expiresIn: '1h',
issuer: 'my-app'
}
);
}
function verifyToken(token) {
try {
return jwt.verify(token, SECRET);
} catch (err) {
if (err.name === 'TokenExpiredError') {
throw new Error('Token 已過期');
}
throw new Error('Token 無效');
}
}
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: '未提供 Token' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = verifyToken(token);
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: err.message });
}
}
為什麼需要兩種 Token?
Access Token:
├── 有效期短(15 分鐘 ~ 1 小時)
├── 用於存取 API
└── 洩漏風險較小
Refresh Token:
├── 有效期長(7 天 ~ 30 天)
├── 僅用於換新 Access Token
└── 應安全儲存
流程:
1. 登入 → 取得 Access Token + Refresh Token
2. Access Token 用於 API 請求
3. Access Token 過期 → 用 Refresh Token 換新
4. Refresh Token 過期 → 重新登入
app.post('/refresh', (req, res) => {
const { refreshToken } = req.body;
try {
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
if (isBlacklisted(refreshToken)) {
return res.status(401).json({ error: 'Token 已被撤銷' });
}
const accessToken = generateAccessToken(decoded.sub);
res.json({ accessToken });
} catch (err) {
res.status(401).json({ error: 'Refresh Token 無效' });
}
});
| 優點 |
缺點 |
| ✅ 無狀態,易於擴展 |
❌ 無法立即撤銷 |
| ✅ 跨域友好 |
❌ Token 較大(相較 Session ID) |
| ✅ 自包含使用者資訊 |
❌ Payload 可解碼(勿放敏感資料) |
| ✅ 標準化 |
❌ 需要處理 Token 刷新邏輯 |
OAuth 2.0 是一個授權框架,允許第三方應用在使用者授權下存取其資源,而不需要知道使用者的密碼。
場景:你想用「Google 帳號登入」某個 App
傳統方式(危險):
App 要你輸入 Google 帳號密碼 → App 知道你的密碼 → 不安全!
OAuth 方式(安全):
App 導向 Google 登入頁 → 你在 Google 輸入密碼 → Google 給 App 授權碼
→ App 不知道你的密碼,只有存取權限
| 角色 |
說明 |
範例 |
| Resource Owner |
資源擁有者(使用者) |
你 |
| Client |
第三方應用 |
某個想用 Google 登入的 App |
| Authorization Server |
授權伺服器 |
Google 授權服務 |
| Resource Server |
資源伺服器 |
Google API(照片、郵件等) |
最安全、最常用的模式,適合有後端伺服器的 Web 應用。
┌─────────┐ ┌─────────────────┐
│ User │ │ Authorization │
│(Browser)│ │ Server │
└────┬────┘ └────────┬────────┘
│ │
│ 1. 點擊「用 Google 登入」 │
│ ─────────────────────────────────────────→ │
│ │
│ 2. 導向 Google 登入頁 │
│ ←───────────────────────────────────────── │
│ │
│ 3. 使用者登入並授權 │
│ ─────────────────────────────────────────→ │
│ │
│ 4. 重導向回 App(帶 Authorization Code) │
│ ←───────────────────────────────────────── │
│ │
┌────┴────┐ ┌────────┴────────┐
│ Client │ │ Authorization │
│ (後端) │ │ Server │
└────┬────┘ └────────┬────────┘
│ │
│ 5. 用 Code 換 Access Token(後端對後端) │
│ ─────────────────────────────────────────→ │
│ │
│ 6. 回傳 Access Token │
│ ←───────────────────────────────────────── │
│ │
│ 7. 用 Access Token 存取 API │
│ ─────────────────────────────────────────→ │
| 模式 |
適用場景 |
安全性 |
| Authorization Code |
Web 應用(有後端) |
⭐⭐⭐⭐⭐ 最高 |
| Authorization Code + PKCE |
SPA、行動 App |
⭐⭐⭐⭐ 高 |
| Client Credentials |
伺服器對伺服器 |
⭐⭐⭐⭐ 高 |
| Implicit(已廢棄) |
SPA(舊方式) |
⭐⭐ 低 |
| Password(已廢棄) |
高度信任的 App |
⭐ 最低 |
解決 SPA 和行動 App 無法安全儲存 Client Secret 的問題。
1. Client 產生隨機 code_verifier
2. 計算 code_challenge = SHA256(code_verifier)
3. 授權請求帶 code_challenge
4. 換 Token 時帶 code_verifier
5. Server 驗證:SHA256(code_verifier) == code_challenge
app.get('/auth/google', (req, res) => {
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: 'http://localhost:3000/auth/google/callback',
response_type: 'code',
scope: 'openid email profile',
state: generateRandomState()
});
res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});
app.get('/auth/google/callback', async (req, res) => {
const { code, state } = req.query;
if (!verifyState(state)) {
return res.status(400).json({ error: 'Invalid state' });
}
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code,
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
redirect_uri: 'http://localhost:3000/auth/google/callback',
grant_type: 'authorization_code'
})
});
const { access_token, id_token } = await tokenResponse.json();
const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${access_token}` }
});
const user = await userResponse.json();
req.session.user = user;
res.redirect('/dashboard');
});
OpenID Connect 是建立在 OAuth 2.0 之上的身份認證層。
OAuth 2.0 = 授權(Authorization)
「允許 App 存取你的照片」
OpenID Connect = OAuth 2.0 + 認證(Authentication)
「證明你是誰 + 允許 App 存取你的照片」
| 元素 |
說明 |
| ID Token |
JWT 格式,包含使用者身份資訊 |
| UserInfo Endpoint |
取得使用者詳細資訊的 API |
| 標準 Scopes |
openid, profile, email 等 |
| 標準 Claims |
sub, name, email 等標準欄位 |
| 項目 |
ID Token |
Access Token |
| 用途 |
證明使用者身份 |
存取 API 資源 |
| 格式 |
一定是 JWT |
可以是任何格式 |
| 給誰看 |
Client(前端) |
Resource Server |
| 內容 |
使用者資訊 |
權限範圍 |
最簡單的 API 認證方式。
curl -H "X-API-Key: your-api-key" https://api.example.com/data
curl https://api.example.com/data?api_key=your-api-key
| 優點 |
缺點 |
| ✅ 實作簡單 |
❌ 無法區分使用者 |
| ✅ 適合伺服器對伺服器 |
❌ 洩漏風險(尤其在前端) |
| ✅ 無狀態 |
❌ 難以細粒度控制權限 |
curl -u username:password https://api.example.com/data
curl -H "Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=" https://api.example.com/data
| 優點 |
缺點 |
| ✅ 標準化(RFC 7617) |
❌ 每次都傳密碼(必須 HTTPS) |
| ✅ 實作簡單 |
❌ 無法撤銷單一 Session |
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." https://api.example.com/data
| 方式 |
安全性 |
複雜度 |
適用場景 |
| API Key |
⭐⭐ |
低 |
伺服器對伺服器、內部 API |
| Basic Auth |
⭐⭐ |
低 |
內部系統、測試環境 |
| Bearer Token (JWT) |
⭐⭐⭐⭐ |
中 |
公開 API、微服務 |
| OAuth 2.0 |
⭐⭐⭐⭐⭐ |
高 |
第三方整合、高安全需求 |
SSO(Single Sign-On)讓使用者只需登入一次,就能存取多個相關但獨立的系統。
沒有 SSO:
├── 登入 Gmail → 輸入帳密
├── 登入 YouTube → 再輸入一次帳密
└── 登入 Google Drive → 又要輸入一次帳密
有 SSO:
├── 登入 Gmail → 輸入帳密 ✓
├── 開啟 YouTube → 已登入 ✓
└── 開啟 Google Drive → 已登入 ✓
| 方式 |
說明 |
適用場景 |
| 共享 Cookie |
同域名下共享 Session Cookie |
同公司不同子系統 |
| OAuth 2.0 / OIDC |
透過中央授權伺服器 |
跨域、第三方整合 |
| SAML 2.0 |
XML-based 企業級方案 |
企業內部、政府機構 |
| CAS |
Central Authentication Service |
大學、企業內部 |
| 項目 |
SAML 2.0 |
OAuth 2.0 |
OIDC |
| 主要用途 |
認證 + SSO |
授權 |
認證 + 授權 |
| 格式 |
XML |
JSON |
JSON (JWT) |
| 年代 |
2005 |
2012 |
2014 |
| 複雜度 |
高 |
中 |
中 |
| 適用場景 |
企業 SSO |
API 授權 |
現代 Web/App |
const password = user.password;
const hash = md5(password);
const bcrypt = require('bcrypt');
const saltRounds = 12;
const hash = await bcrypt.hash(password, saltRounds);
const isMatch = await bcrypt.compare(password, hash);
| 儲存位置 |
XSS 風險 |
CSRF 風險 |
建議 |
| localStorage |
⚠️ 高 |
✅ 無 |
不建議敏感 Token |
| sessionStorage |
⚠️ 高 |
✅ 無 |
不建議敏感 Token |
| Cookie (HttpOnly) |
✅ 低 |
⚠️ 需防範 |
推薦 + SameSite |
| 記憶體 |
✅ 無 |
✅ 無 |
最安全但重整後消失 |
res.cookie('token', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000,
path: '/',
domain: '.example.com'
});
| 項目 |
說明 |
| ✅ 密碼使用 bcrypt/argon2 加密 |
不用 MD5/SHA1 |
| ✅ 使用 HTTPS |
所有傳輸加密 |
| ✅ Token 設定合理過期時間 |
Access: 15 分鐘~1 小時 |
| ✅ 實作 Refresh Token 機制 |
避免頻繁重新登入 |
| ✅ Cookie 設定 HttpOnly + Secure |
防止 XSS |
| ✅ 實作 CSRF 防護 |
使用 CSRF Token 或 SameSite |
| ✅ 實作 Rate Limiting |
防止暴力破解 |
| ✅ 登入失敗鎖定機制 |
多次失敗後暫時鎖定 |
| ✅ 敏感操作需重新驗證 |
修改密碼、刪除帳號等 |
解決方案:
1. 短期 Access Token + Refresh Token
└─ Access Token 15 分鐘過期,降低風險
2. Token 黑名單
└─ 維護已撤銷的 Token 清單(Redis)
3. Token 版本號
└─ 使用者資料庫存 tokenVersion,每次登出 +1
└─ 驗證時比對 Token 中的版本號
1. 使用者輸入 Email
2. 產生一次性 Token(存 Redis,設 15 分鐘過期)
3. 發送重設連結到 Email
4. 使用者點擊連結,驗證 Token
5. 輸入新密碼
6. 刪除 Token,更新密碼
防止 CSRF 攻擊!
沒有 state:
1. 攻擊者誘導你點擊他準備的授權連結
2. 你登入後,授權碼發到攻擊者的網站
3. 攻擊者用你的身份登入
有 state:
1. Client 產生隨機 state,存在 Session
2. 授權請求帶上 state
3. 回調時驗證 state 是否一致
4. 攻擊者無法預測你的 state → 攻擊失敗
| 場景 |
推薦方案 |
| 傳統 Web 應用 |
Session + Cookie |
| SPA + API |
JWT + Refresh Token |
| 微服務 |
JWT + OAuth 2.0 |
| 第三方登入 |
OAuth 2.0 + OIDC |
| 行動 App |
OAuth 2.0 + PKCE |
| 伺服器對伺服器 |
OAuth 2.0 Client Credentials 或 API Key |
| 企業 SSO |
SAML 2.0 或 OIDC |
| 術語 |
說明 |
| Authentication (AuthN) |
認證:你是誰? |
| Authorization (AuthZ) |
授權:你能做什麼? |
| Session |
伺服器端儲存的會話狀態 |
| Token |
客戶端持有的憑證 |
| JWT |
JSON Web Token,自包含的 Token 格式 |
| OAuth 2.0 |
授權框架 |
| OIDC |
OAuth 2.0 + 身份認證 |
| SSO |
單一登入 |
| SAML |
企業級 SSO 協定 |
| 標頭 |
格式 |
用途 |
| Authorization |
Basic base64(user:pass) |
Basic 認證 |
| Authorization |
Bearer <token> |
Token 認證 |
| Cookie |
sessionId=abc123 |
Session 認證 |
| X-API-Key |
your-api-key |
API Key |
建立日期:2025-12-04
最後更新:2025-12-04