認證與授權完全指南

完整介紹認證(Authentication)與授權(Authorization)的概念、各種方案比較與實作指南


目錄

  1. 認證 vs 授權
  2. 認證方式總覽
  3. Session 認證
  4. Token 認證
  5. JWT 詳解
  6. OAuth 2.0
  7. OpenID Connect
  8. API 認證方式
  9. SSO 單一登入
  10. 最佳實踐
  11. 常見問題
  12. 總結

認證 vs 授權

基本概念

概念 英文 問題 比喻
認證 Authentication (AuthN) 你是誰? 出示身分證
授權 Authorization (AuthZ) 你能做什麼? 檢查門禁卡權限
認證(Authentication)
├── 驗證使用者身份
├── 「證明你是你」
└── 例:輸入帳號密碼登入

授權(Authorization)
├── 決定使用者能存取什麼資源
├── 「決定你能做什麼」
└── 例:管理員可以刪除文章,一般用戶不行

流程順序

使用者請求
1. 認證(Authentication)
   「你是誰?請證明身份」
   ├── 成功 → 繼續
   └── 失敗 → 401 Unauthorized
2. 授權(Authorization)
   「你有權限做這件事嗎?」
   ├── 有權限 → 執行操作
   └── 無權限 → 403 Forbidden

HTTP 狀態碼

狀態碼 名稱 意義
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

Session 認證

運作流程

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 查詢使用者資訊

實作範例

// Express.js Session 設定
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,      // 僅 HTTPS
    httpOnly: true,    // 防止 XSS
    maxAge: 3600000,   // 1 小時
    sameSite: 'strict' // 防止 CSRF
  }
}));

// 登入
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 優缺點

優點 缺點
✅ 伺服器完全控制 ❌ 有狀態,需要儲存空間
✅ 可立即撤銷(登出) ❌ 水平擴展需共享 Session
✅ 資料存伺服器,安全 ❌ CSRF 攻擊風險
✅ 實作相對簡單 ❌ 不適合跨域/行動 App

Token 認證

運作流程

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 簽章,取出使用者資訊

Token vs Session

比較項目 Session Token
狀態 有狀態(Stateful) 無狀態(Stateless)
儲存位置 伺服器端 客戶端
擴展性 需共享 Session Store 天生支援水平擴展
撤銷 立即撤銷 需額外機制
跨域 困難 簡單
適用場景 傳統 Web API、微服務、SPA

JWT 詳解

什麼是 JWT?

JWT(JSON Web Token)是一種開放標準(RFC 7519),用於在各方之間安全地傳輸資訊。

JWT 結構

JWT = Header.Payload.Signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.    ← Header
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikp...  ← Payload
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c  ← Signature

1. Header(標頭)

{
  "alg": "HS256",    // 簽章演算法
  "typ": "JWT"       // Token 類型
}

2. Payload(酬載)

{
  // 註冊宣告(標準欄位)
  "iss": "auth-server",      // Issuer 簽發者
  "sub": "1234567890",       // Subject 主體(通常是 user ID)
  "aud": "my-app",           // Audience 受眾
  "exp": 1735689600,         // Expiration 到期時間
  "iat": 1735686000,         // Issued At 簽發時間
  "nbf": 1735686000,         // Not Before 生效時間

  // 自訂宣告
  "name": "John Doe",
  "role": "admin"
}

3. Signature(簽章)

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

JWT 驗證流程

Client 發送 JWT
1. 解碼 Header 和 Payload(Base64)
2. 使用相同演算法和密鑰重新計算簽章
3. 比對簽章是否一致
   ├── 一致 → Token 未被竄改
   └── 不一致 → Token 無效
4. 檢查 exp(是否過期)
5. 取出使用者資訊

JWT 實作範例

const jwt = require('jsonwebtoken');

const SECRET = process.env.JWT_SECRET;

// 產生 JWT
function generateToken(user) {
  return jwt.sign(
    {
      sub: user.id,
      name: user.name,
      role: user.role
    },
    SECRET,
    {
      expiresIn: '1h',      // 1 小時後過期
      issuer: 'my-app'
    }
  );
}

// 驗證 JWT
function verifyToken(token) {
  try {
    return jwt.verify(token, SECRET);
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      throw new Error('Token 已過期');
    }
    throw new Error('Token 無效');
  }
}

// Middleware
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 });
  }
}

Access Token + Refresh Token

為什麼需要兩種 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 過期 → 重新登入
// 刷新 Token 端點
app.post('/refresh', (req, res) => {
  const { refreshToken } = req.body;

  try {
    const decoded = jwt.verify(refreshToken, REFRESH_SECRET);

    // 檢查 Refresh Token 是否在黑名單
    if (isBlacklisted(refreshToken)) {
      return res.status(401).json({ error: 'Token 已被撤銷' });
    }

    // 產生新的 Access Token
    const accessToken = generateAccessToken(decoded.sub);

    res.json({ accessToken });
  } catch (err) {
    res.status(401).json({ error: 'Refresh Token 無效' });
  }
});

JWT 優缺點

優點 缺點
✅ 無狀態,易於擴展 ❌ 無法立即撤銷
✅ 跨域友好 ❌ Token 較大(相較 Session ID)
✅ 自包含使用者資訊 ❌ Payload 可解碼(勿放敏感資料)
✅ 標準化 ❌ 需要處理 Token 刷新邏輯

OAuth 2.0

什麼是 OAuth 2.0?

OAuth 2.0 是一個授權框架,允許第三方應用在使用者授權下存取其資源,而不需要知道使用者的密碼

場景:你想用「Google 帳號登入」某個 App

傳統方式(危險):
App 要你輸入 Google 帳號密碼 → App 知道你的密碼 → 不安全!

OAuth 方式(安全):
App 導向 Google 登入頁 → 你在 Google 輸入密碼 → Google 給 App 授權碼
→ App 不知道你的密碼,只有存取權限

OAuth 2.0 角色

角色 說明 範例
Resource Owner 資源擁有者(使用者)
Client 第三方應用 某個想用 Google 登入的 App
Authorization Server 授權伺服器 Google 授權服務
Resource Server 資源伺服器 Google API(照片、郵件等)

授權碼模式(Authorization Code)

最安全、最常用的模式,適合有後端伺服器的 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                │
          │ ─────────────────────────────────────────→ │

OAuth 2.0 四種授權模式

模式 適用場景 安全性
Authorization Code Web 應用(有後端) ⭐⭐⭐⭐⭐ 最高
Authorization Code + PKCE SPA、行動 App ⭐⭐⭐⭐ 高
Client Credentials 伺服器對伺服器 ⭐⭐⭐⭐ 高
Implicit(已廢棄) SPA(舊方式) ⭐⭐ 低
Password(已廢棄) 高度信任的 App ⭐ 最低

PKCE(Proof Key for Code Exchange)

解決 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

OAuth 2.0 實作範例

// 1. 導向授權頁面
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() // 防 CSRF
  });

  res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});

// 2. 處理回調
app.get('/auth/google/callback', async (req, res) => {
  const { code, state } = req.query;

  // 驗證 state
  if (!verifyState(state)) {
    return res.status(400).json({ error: 'Invalid state' });
  }

  // 3. 用 code 換 token
  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();

  // 4. 用 access_token 取得使用者資訊
  const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
    headers: { Authorization: `Bearer ${access_token}` }
  });

  const user = await userResponse.json();

  // 5. 建立本地 session 或 JWT
  req.session.user = user;
  res.redirect('/dashboard');
});

OpenID Connect

什麼是 OpenID Connect(OIDC)?

OpenID Connect 是建立在 OAuth 2.0 之上的身份認證層

OAuth 2.0 = 授權(Authorization)
           「允許 App 存取你的照片」

OpenID Connect = OAuth 2.0 + 認證(Authentication)
                「證明你是誰 + 允許 App 存取你的照片」

OIDC 新增的東西

元素 說明
ID Token JWT 格式,包含使用者身份資訊
UserInfo Endpoint 取得使用者詳細資訊的 API
標準 Scopes openid, profile, email
標準 Claims sub, name, email 等標準欄位

ID Token vs Access Token

項目 ID Token Access Token
用途 證明使用者身份 存取 API 資源
格式 一定是 JWT 可以是任何格式
給誰看 Client(前端) Resource Server
內容 使用者資訊 權限範圍

API 認證方式

1. API Key

最簡單的 API 認證方式。

# 請求範例
curl -H "X-API-Key: your-api-key" https://api.example.com/data

# 或放在 Query String(較不安全)
curl https://api.example.com/data?api_key=your-api-key
優點 缺點
✅ 實作簡單 ❌ 無法區分使用者
✅ 適合伺服器對伺服器 ❌ 洩漏風險(尤其在前端)
✅ 無狀態 ❌ 難以細粒度控制權限

2. Basic Authentication

# 格式:base64(username:password)
curl -u username:password https://api.example.com/data

# 等同於
curl -H "Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=" https://api.example.com/data
優點 缺點
✅ 標準化(RFC 7617) ❌ 每次都傳密碼(必須 HTTPS)
✅ 實作簡單 ❌ 無法撤銷單一 Session

3. Bearer Token

curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." https://api.example.com/data

API 認證方式比較

方式 安全性 複雜度 適用場景
API Key ⭐⭐ 伺服器對伺服器、內部 API
Basic Auth ⭐⭐ 內部系統、測試環境
Bearer Token (JWT) ⭐⭐⭐⭐ 公開 API、微服務
OAuth 2.0 ⭐⭐⭐⭐⭐ 第三方整合、高安全需求

SSO 單一登入

什麼是 SSO?

SSO(Single Sign-On)讓使用者只需登入一次,就能存取多個相關但獨立的系統。

沒有 SSO:
├── 登入 Gmail → 輸入帳密
├── 登入 YouTube → 再輸入一次帳密
└── 登入 Google Drive → 又要輸入一次帳密

有 SSO:
├── 登入 Gmail → 輸入帳密 ✓
├── 開啟 YouTube → 已登入 ✓
└── 開啟 Google Drive → 已登入 ✓

SSO 實作方式

方式 說明 適用場景
共享 Cookie 同域名下共享 Session Cookie 同公司不同子系統
OAuth 2.0 / OIDC 透過中央授權伺服器 跨域、第三方整合
SAML 2.0 XML-based 企業級方案 企業內部、政府機構
CAS Central Authentication Service 大學、企業內部

SAML vs OAuth 2.0 vs OIDC

項目 SAML 2.0 OAuth 2.0 OIDC
主要用途 認證 + SSO 授權 認證 + 授權
格式 XML JSON JSON (JWT)
年代 2005 2012 2014
複雜度
適用場景 企業 SSO API 授權 現代 Web/App

最佳實踐

密碼安全

// ❌ 錯誤:明文儲存密碼
const password = user.password;

// ❌ 錯誤:使用 MD5/SHA1
const hash = md5(password);

// ✅ 正確:使用 bcrypt
const bcrypt = require('bcrypt');
const saltRounds = 12;

// 加密
const hash = await bcrypt.hash(password, saltRounds);

// 驗證
const isMatch = await bcrypt.compare(password, hash);

Token 儲存

儲存位置 XSS 風險 CSRF 風險 建議
localStorage ⚠️ 高 ✅ 無 不建議敏感 Token
sessionStorage ⚠️ 高 ✅ 無 不建議敏感 Token
Cookie (HttpOnly) ✅ 低 ⚠️ 需防範 推薦 + SameSite
記憶體 ✅ 無 ✅ 無 最安全但重整後消失
// 安全的 Cookie 設定
res.cookie('token', accessToken, {
  httpOnly: true,    // 防止 JavaScript 存取(防 XSS)
  secure: true,      // 僅 HTTPS 傳輸
  sameSite: 'strict', // 防止 CSRF
  maxAge: 3600000,   // 1 小時
  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:JWT 無法撤銷怎麼辦?

解決方案:

1. 短期 Access Token + Refresh Token
   └─ Access Token 15 分鐘過期,降低風險

2. Token 黑名單
   └─ 維護已撤銷的 Token 清單(Redis)

3. Token 版本號
   └─ 使用者資料庫存 tokenVersion,每次登出 +1
   └─ 驗證時比對 Token 中的版本號

問題 2:忘記密碼流程怎麼實作?

1. 使用者輸入 Email
2. 產生一次性 Token(存 Redis,設 15 分鐘過期)
3. 發送重設連結到 Email
4. 使用者點擊連結,驗證 Token
5. 輸入新密碼
6. 刪除 Token,更新密碼

問題 3:OAuth 2.0 的 state 參數有什麼用?

防止 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 協定

HTTP 標頭

標頭 格式 用途
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

🔗相關文章