API 認證方式深度比較:API Key vs Session vs JWT

理解三種主流認證機制的原理、優缺點與使用場景

認證機制的重要性

在現代 API 設計中,選擇合適的認證機制是確保系統安全的第一步。不同的認證方式有著截然不同的特性,適用於不同的場景。

讓我們深入比較 API Key、Session 和 JWT 這三種最常用的認證方式。

📊 三種認證方式對比表

根據題目提供的比較維度,我們先看看整體對比:

特性API KeySessionJWT
無狀態性通常無(需查詢資料庫)是(本地驗證)
權限資訊豐富內嵌
撤銷難易度容易(伺服器端檢查)極佳複雜
可擴展性優秀(共享快取)需要共享儲存優秀

現在讓我們深入了解每種認證方式。

🔑 API Key 認證

什麼是 API Key?

API Key 是一個唯一的字串,用於識別和認證 API 的呼叫者。它就像是一把進入系統的鑰匙。

範例:
API-Key: sk_live_4eC39HqLyjWDarjtT1zdp7dc

實作原理

# Flask 實作 API Key 認證
from functools import wraps
from flask import request, jsonify
import hashlib

class APIKeyAuth:
    def __init__(self, db):
        self.db = db
        self.cache = {}  # 簡單的記憶體快取
    
    def generate_api_key(self, user_id, name="default"):
        """生成 API Key"""
        # 生成隨機 key
        import secrets
        raw_key = secrets.token_urlsafe(32)
        
        # 儲存時使用 hash(安全考量)
        key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
        
        # 儲存到資料庫
        self.db.execute("""
            INSERT INTO api_keys (user_id, key_hash, name, created_at)
            VALUES (?, ?, ?, datetime('now'))
        """, (user_id, key_hash, name))
        
        # 返回原始 key(只顯示一次)
        return f"sk_live_{raw_key}"
    
    def verify_api_key(self, api_key):
        """驗證 API Key"""
        # 檢查快取
        if api_key in self.cache:
            return self.cache[api_key]
        
        # 移除前綴
        if api_key.startswith("sk_live_"):
            raw_key = api_key[8:]
        else:
            return None
        
        # 計算 hash
        key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
        
        # 查詢資料庫
        result = self.db.execute("""
            SELECT user_id, scopes, rate_limit
            FROM api_keys
            WHERE key_hash = ? AND revoked = 0
        """, (key_hash,)).fetchone()
        
        if result:
            # 加入快取
            self.cache[api_key] = result
            return result
        
        return None

# 裝飾器
def require_api_key(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        api_key = request.headers.get('X-API-Key')
        
        if not api_key:
            return jsonify({'error': 'API key required'}), 401
        
        auth_result = api_key_auth.verify_api_key(api_key)
        if not auth_result:
            return jsonify({'error': 'Invalid API key'}), 401
        
        # 將認證資訊加入請求上下文
        request.api_key_info = auth_result
        return f(*args, **kwargs)
    
    return decorated_function

# 使用範例
@app.route('/api/data')
@require_api_key
def get_data():
    user_id = request.api_key_info['user_id']
    return jsonify({'message': f'Hello user {user_id}'})

優缺點分析

優點:
  ✅ 實作簡單
  ✅ 適合 B2B API
  ✅ 易於撤銷(更新資料庫即可)
  ✅ 可以設定細粒度權限
  ✅ 支援多個 key(不同用途)

缺點:
  ❌ 每次請求需要查詢資料庫(除非有快取)
  ❌ Key 洩漏風險
  ❌ 不適合瀏覽器端使用
  ❌ 無法攜帶豐富的用戶資訊

最佳實踐

class EnhancedAPIKeyAuth:
    def __init__(self):
        self.redis = redis.Redis()
    
    def verify_with_rate_limit(self, api_key):
        """包含速率限制的驗證"""
        # 驗證 key
        key_info = self.verify_api_key(api_key)
        if not key_info:
            return None, "Invalid key"
        
        # 檢查速率限制
        rate_limit_key = f"rate_limit:{api_key}"
        current_count = self.redis.incr(rate_limit_key)
        
        if current_count == 1:
            # 設定過期時間(1小時)
            self.redis.expire(rate_limit_key, 3600)
        
        if current_count > key_info['rate_limit']:
            return None, "Rate limit exceeded"
        
        return key_info, None
    
    def rotate_key(self, old_key, grace_period_days=7):
        """金鑰輪換"""
        # 生成新 key
        new_key = self.generate_api_key(user_id)
        
        # 設定舊 key 過期時間
        expire_date = datetime.now() + timedelta(days=grace_period_days)
        self.db.execute("""
            UPDATE api_keys 
            SET expires_at = ? 
            WHERE key_hash = ?
        """, (expire_date, old_key_hash))
        
        return new_key

🍪 Session 認證

什麼是 Session?

Session 是伺服器端儲存用戶狀態的機制,通過 Session ID 來識別用戶。

實作原理

# Flask-Session 實作
from flask import Flask, session, request, jsonify
from flask_session import Session
import redis

app = Flask(__name__)

# 設定 Session
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = redis.Redis(host='localhost', port=6379)
app.config['SESSION_PERMANENT'] = False
app.config['SESSION_USE_SIGNER'] = True
app.config['SESSION_KEY_PREFIX'] = 'myapp:'
app.config['SECRET_KEY'] = 'your-secret-key'

Session(app)

@app.route('/api/login', methods=['POST'])
def login():
    username = request.json.get('username')
    password = request.json.get('password')
    
    # 驗證用戶
    user = authenticate_user(username, password)
    if not user:
        return jsonify({'error': 'Invalid credentials'}), 401
    
    # 建立 Session
    session['user_id'] = user['id']
    session['username'] = user['username']
    session['roles'] = user['roles']
    session['login_time'] = datetime.utcnow().isoformat()
    
    return jsonify({
        'message': 'Login successful',
        'user': {
            'id': user['id'],
            'username': user['username']
        }
    })

@app.route('/api/profile')
def get_profile():
    if 'user_id' not in session:
        return jsonify({'error': 'Not authenticated'}), 401
    
    # Session 中已有豐富資訊
    return jsonify({
        'user_id': session['user_id'],
        'username': session['username'],
        'roles': session['roles'],
        'login_time': session['login_time']
    })

@app.route('/api/logout', methods=['POST'])
def logout():
    session.clear()
    return jsonify({'message': 'Logged out successfully'})

Session 共享(分散式環境)

class DistributedSessionManager:
    """分散式 Session 管理"""
    
    def __init__(self):
        # 使用 Redis 作為共享儲存
        self.redis = redis.Redis(
            host='redis-cluster',
            port=6379,
            decode_responses=True
        )
        self.ttl = 3600  # 1 小時
    
    def create_session(self, user_data):
        """建立 Session"""
        import uuid
        session_id = str(uuid.uuid4())
        
        # 儲存到 Redis
        session_key = f"session:{session_id}"
        self.redis.hmset(session_key, user_data)
        self.redis.expire(session_key, self.ttl)
        
        return session_id
    
    def get_session(self, session_id):
        """獲取 Session"""
        session_key = f"session:{session_id}"
        return self.redis.hgetall(session_key)
    
    def update_session(self, session_id, data):
        """更新 Session"""
        session_key = f"session:{session_id}"
        self.redis.hmset(session_key, data)
        # 重置過期時間
        self.redis.expire(session_key, self.ttl)
    
    def destroy_session(self, session_id):
        """銷毀 Session"""
        session_key = f"session:{session_id}"
        self.redis.delete(session_key)

# 中間件
@app.before_request
def load_session():
    session_id = request.cookies.get('session_id')
    if session_id:
        session_data = session_manager.get_session(session_id)
        if session_data:
            request.session = session_data
        else:
            request.session = {}
    else:
        request.session = {}

@app.after_request
def save_session(response):
    if hasattr(request, 'session_modified') and request.session_modified:
        if 'session_id' not in request.cookies:
            # 建立新 Session
            session_id = session_manager.create_session(request.session)
            response.set_cookie(
                'session_id', 
                session_id,
                httponly=True,
                secure=True,
                samesite='Lax'
            )
        else:
            # 更新現有 Session
            session_id = request.cookies.get('session_id')
            session_manager.update_session(session_id, request.session)
    
    return response

優缺點分析

優點:
  ✅ 可儲存豐富的用戶資訊
  ✅ 易於撤銷(刪除 Session)
  ✅ 支援複雜的權限管理
  ✅ 傳統 Web 應用成熟方案

缺點:
  ❌ 有狀態(違反 REST 原則)
  ❌ 擴展性差(需要 Session 共享)
  ❌ 跨域問題(Cookie 限制)
  ❌ 額外的儲存成本

🎫 JWT(JSON Web Token)認證

什麼是 JWT?

JWT 是一個自包含的令牌,包含了認證和授權所需的所有資訊。

JWT 結構

Header.Payload.Signature

Header:
  {
    "alg": "HS256",
    "typ": "JWT"
  }

Payload:
  {
    "sub": "1234567890",
    "name": "John Doe",
    "roles": ["admin", "user"],
    "iat": 1516239022,
    "exp": 1516242622
  }

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

實作原理

import jwt
from datetime import datetime, timedelta, timezone
from functools import wraps

class JWTAuth:
    def __init__(self, secret_key, algorithm='HS256'):
        self.secret_key = secret_key
        self.algorithm = algorithm
        self.access_token_expire = timedelta(minutes=15)
        self.refresh_token_expire = timedelta(days=7)
    
    def generate_tokens(self, user):
        """生成 access token 和 refresh token"""
        now = datetime.now(timezone.utc)
        
        # Access Token(短期)
        access_payload = {
            'user_id': user['id'],
            'username': user['username'],
            'roles': user['roles'],
            'type': 'access',
            'iat': now,
            'exp': now + self.access_token_expire
        }
        access_token = jwt.encode(
            access_payload, 
            self.secret_key, 
            algorithm=self.algorithm
        )
        
        # Refresh Token(長期)
        refresh_payload = {
            'user_id': user['id'],
            'type': 'refresh',
            'iat': now,
            'exp': now + self.refresh_token_expire
        }
        refresh_token = jwt.encode(
            refresh_payload,
            self.secret_key,
            algorithm=self.algorithm
        )
        
        return access_token, refresh_token
    
    def verify_token(self, token, token_type='access'):
        """驗證 token"""
        try:
            payload = jwt.decode(
                token,
                self.secret_key,
                algorithms=[self.algorithm]
            )
            
            # 檢查 token 類型
            if payload.get('type') != token_type:
                return None, 'Invalid token type'
            
            return payload, None
            
        except jwt.ExpiredSignatureError:
            return None, 'Token has expired'
        except jwt.InvalidTokenError as e:
            return None, f'Invalid token: {str(e)}'

# 裝飾器
def jwt_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        auth_header = request.headers.get('Authorization')
        
        if not auth_header or not auth_header.startswith('Bearer '):
            return jsonify({'error': 'Token required'}), 401
        
        token = auth_header.split(' ')[1]
        payload, error = jwt_auth.verify_token(token)
        
        if error:
            return jsonify({'error': error}), 401
        
        request.jwt_payload = payload
        return f(*args, **kwargs)
    
    return decorated_function

# API 端點
@app.route('/api/login', methods=['POST'])
def login():
    username = request.json.get('username')
    password = request.json.get('password')
    
    user = authenticate_user(username, password)
    if not user:
        return jsonify({'error': 'Invalid credentials'}), 401
    
    access_token, refresh_token = jwt_auth.generate_tokens(user)
    
    return jsonify({
        'access_token': access_token,
        'refresh_token': refresh_token,
        'token_type': 'bearer'
    })

@app.route('/api/refresh', methods=['POST'])
def refresh():
    refresh_token = request.json.get('refresh_token')
    
    payload, error = jwt_auth.verify_token(refresh_token, 'refresh')
    if error:
        return jsonify({'error': error}), 401
    
    # 獲取用戶最新資訊
    user = get_user(payload['user_id'])
    access_token, _ = jwt_auth.generate_tokens(user)
    
    return jsonify({'access_token': access_token})

@app.route('/api/protected')
@jwt_required
def protected_route():
    return jsonify({
        'message': 'Success',
        'user': request.jwt_payload
    })

JWT 撤銷機制(複雜但可行)

class JWTRevocation:
    """JWT 撤銷管理"""
    
    def __init__(self, redis_client):
        self.redis = redis_client
    
    def revoke_token(self, jti, exp):
        """撤銷 token"""
        # 計算剩餘有效時間
        ttl = exp - datetime.now(timezone.utc)
        if ttl > timedelta(seconds=0):
            # 加入黑名單
            self.redis.setex(
                f"revoked:{jti}",
                int(ttl.total_seconds()),
                '1'
            )
    
    def is_token_revoked(self, jti):
        """檢查 token 是否被撤銷"""
        return self.redis.exists(f"revoked:{jti}")
    
    def revoke_all_user_tokens(self, user_id):
        """撤銷用戶的所有 token"""
        # 記錄撤銷時間
        revoke_time = datetime.now(timezone.utc)
        self.redis.set(
            f"user_revoked:{user_id}",
            revoke_time.timestamp()
        )

# 增強的驗證
def verify_token_with_revocation(token):
    payload, error = jwt_auth.verify_token(token)
    if error:
        return None, error
    
    # 檢查 token 是否被撤銷
    jti = payload.get('jti')
    if jti and revocation.is_token_revoked(jti):
        return None, 'Token has been revoked'
    
    # 檢查用戶級撤銷
    user_id = payload.get('user_id')
    revoke_time = revocation.redis.get(f"user_revoked:{user_id}")
    if revoke_time:
        iat = payload.get('iat')
        if iat < float(revoke_time):
            return None, 'All user tokens have been revoked'
    
    return payload, None

優缺點分析

優點:
  ✅ 無狀態(本地驗證)
  ✅ 優秀的可擴展性
  ✅ 自包含(攜帶用戶資訊)
  ✅ 跨域友好
  ✅ 適合微服務架構

缺點:
  ❌ Token 較大
  ❌ 無法真正撤銷(除非加入黑名單)
  ❌ Payload 資訊暴露(Base64)
  ❌ 需要處理 Token 更新

🔄 實際應用場景對比

選擇指南

場景推薦方案原因
第三方 API 整合API Key簡單、易於管理
傳統 Web 應用Session豐富的狀態管理
SPA/Mobile AppJWT無狀態、跨平台
微服務架構JWT分散式友好
高安全要求Session + CSRF易於撤銷

混合使用策略

class HybridAuth:
    """混合認證策略"""
    
    def __init__(self):
        self.jwt_auth = JWTAuth()
        self.session_auth = SessionAuth()
        self.api_key_auth = APIKeyAuth()
    
    def authenticate(self, request):
        """根據請求類型選擇認證方式"""
        
        # API Key(用於 API 客戶端)
        if 'X-API-Key' in request.headers:
            return self.api_key_auth.verify(
                request.headers['X-API-Key']
            )
        
        # JWT(用於 SPA/Mobile)
        if request.headers.get('Authorization', '').startswith('Bearer '):
            return self.jwt_auth.verify(
                request.headers['Authorization'][7:]
            )
        
        # Session(用於傳統 Web)
        if 'session_id' in request.cookies:
            return self.session_auth.verify(
                request.cookies['session_id']
            )
        
        return None, 'No valid authentication provided'

🛡️ 安全性最佳實踐

1. API Key 安全

# 使用環境變數
API_KEY = os.environ.get('API_KEY')

# Key 格式設計
def generate_secure_api_key():
    prefix = 'sk_live_'  # 標識生產環境
    random_part = secrets.token_urlsafe(32)
    checksum = hashlib.sha256(random_part.encode()).hexdigest()[:8]
    return f"{prefix}{random_part}_{checksum}"

# HTTPS 強制
@app.before_request
def force_https():
    if not request.is_secure:
        return jsonify({'error': 'HTTPS required'}), 403

2. Session 安全

# Session 配置
app.config.update(
    SESSION_COOKIE_SECURE=True,      # HTTPS only
    SESSION_COOKIE_HTTPONLY=True,    # 防止 XSS
    SESSION_COOKIE_SAMESITE='Lax',   # CSRF 保護
    PERMANENT_SESSION_LIFETIME=3600   # 1 小時過期
)

# CSRF 保護
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)

3. JWT 安全

# 使用 RS256 而非 HS256
class SecureJWT:
    def __init__(self):
        # 讀取密鑰對
        with open('private_key.pem', 'rb') as f:
            self.private_key = f.read()
        with open('public_key.pem', 'rb') as f:
            self.public_key = f.read()
    
    def generate_token(self, payload):
        # 添加安全聲明
        payload.update({
            'jti': str(uuid.uuid4()),  # 唯一 ID
            'iss': 'https://api.example.com',  # 發行者
            'aud': 'https://app.example.com',  # 受眾
        })
        
        return jwt.encode(
            payload,
            self.private_key,
            algorithm='RS256'
        )

📈 效能考量

快取策略比較

# API Key 快取
class CachedAPIKeyAuth:
    def __init__(self):
        self.cache = TTLCache(maxsize=10000, ttl=300)  # 5分鐘
    
    @cached(cache)
    def verify_api_key(self, key_hash):
        return db.query("SELECT * FROM api_keys WHERE hash = ?", key_hash)

# Session 快取(Redis)
SESSION_REDIS = redis.Redis(
    connection_pool=redis.ConnectionPool(
        max_connections=100,
        decode_responses=True
    )
)

# JWT 無需快取(本地驗證)
# 但可快取公鑰(使用 JWK)
@lru_cache(maxsize=10)
def get_public_key(kid):
    return fetch_public_key_from_jwks(kid)

🎯 總結

三種認證方式各有特色:

API Key

  • 適合:B2B API、內部服務
  • 特點:簡單但需要資料庫查詢

Session

  • 適合:傳統 Web 應用
  • 特點:狀態豐富但擴展性差

JWT

  • 適合:現代 SPA、微服務
  • 特點:無狀態但撤銷複雜

選擇認證方式時,需要考慮:

  1. 應用類型(Web/API/Mobile)
  2. 擴展性需求
  3. 安全性要求
  4. 開發複雜度

沒有完美的方案,只有最適合的選擇。


🔗 延伸閱讀

  • 📖 《OAuth 2.0 in Action》- 深入了解現代認證
  • 📄 JWT.io - JWT 規範與工具
  • 🎥 Auth0 Blog - 認證最佳實踐
  • 💻 開源專案:Passport.js - Node.js 認證中介軟體
0%