API 認證方式深度比較:API Key vs Session vs JWT
理解三種主流認證機制的原理、優缺點與使用場景
目錄
認證機制的重要性
在現代 API 設計中,選擇合適的認證機制是確保系統安全的第一步。不同的認證方式有著截然不同的特性,適用於不同的場景。
讓我們深入比較 API Key、Session 和 JWT 這三種最常用的認證方式。
📊 三種認證方式對比表
根據題目提供的比較維度,我們先看看整體對比:
特性 | API Key | Session | JWT |
---|---|---|---|
無狀態性 | 通常無(需查詢資料庫) | 無 | 是(本地驗證) |
權限資訊 | 無 | 豐富 | 內嵌 |
撤銷難易度 | 容易(伺服器端檢查) | 極佳 | 複雜 |
可擴展性 | 優秀(共享快取) | 需要共享儲存 | 優秀 |
現在讓我們深入了解每種認證方式。
🔑 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 App | JWT | 無狀態、跨平台 |
微服務架構 | 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、微服務
- 特點:無狀態但撤銷複雜
選擇認證方式時,需要考慮:
- 應用類型(Web/API/Mobile)
- 擴展性需求
- 安全性要求
- 開發複雜度
沒有完美的方案,只有最適合的選擇。
🔗 延伸閱讀
- 📖 《OAuth 2.0 in Action》- 深入了解現代認證
- 📄 JWT.io - JWT 規範與工具
- 🎥 Auth0 Blog - 認證最佳實踐
- 💻 開源專案:Passport.js - Node.js 認證中介軟體