FastAPI 課程 – 安全性(Security)
主題:OAuth2 Scopes
簡介
在現代的 Web API 中,授權(Authorization)與認證(Authentication)往往是同時出現的需求。FastAPI 內建支援 OAuth2 流程,讓開發者能以最少的程式碼實作安全的 API。
其中 Scope(範圍)是 OAuth2 的重要概念,它允許我們在同一個 token 內劃分不同的權限層級,從而達成「最小權限原則」的設計。
掌握 OAuth2 Scopes 不僅可以避免過度授權的安全風險,還能讓前端開發者在取得 token 時自行決定需要的功能集合,提升 API 的彈性與可維護性。因此,本單元將深入說明 FastAPI 中如何設定、驗證與使用 Scopes。
核心概念
1. OAuth2 Scope 是什麼?
- Scope 是一個字串集合,用來描述 token 所擁有的權限。例如
["read_items", "write_items"]表示此 token 可以讀取與寫入項目。 - 在 OAuth2 授權請求時,客戶端會透過
scope參數告訴授權伺服器它需要的權限,授權伺服器則根據使用者同意的範圍產生相對應的 token。 - FastAPI 透過
OAuth2PasswordBearer與Security類別,讓我們在路由層級直接宣告需要的 scope。
2. FastAPI 中的 Scope 設定流程
- 建立 OAuth2PasswordBearer,並在
tokenUrl中指定取得 token 的端點。 - 在 token 產生邏輯(通常是
/token)中,根據使用者的角色或權限決定回傳的 scope。 - 在受保護的路由 使用
Security,傳入scopes參數,FastAPI 會自動檢查 token 中是否包含所需的 scope。 - 若檢查失敗,FastAPI 會拋出
HTTPException(status_code=401, detail="Not enough permissions")。
3. Scope 與 Role 的差異
| 項目 | Scope | Role |
|---|---|---|
| 定義方式 | 權限的原子單位(如 read, write) |
使用者的身份集合(如 admin, editor) |
| 彈性 | 可自由組合,多個 scope 組成一個較大權限 | 角色通常固定,較難細分 |
| 使用情境 | 需要對同一資源提供細粒度控制時 | 需要快速判斷使用者身份時 |
在實務上,我們常把 Role 轉換為預設的 Scope 集合,再交給 FastAPI 進行細部驗證。
程式碼範例
以下範例使用 Python(FastAPI)展示如何從頭到尾完成 OAuth2 Scopes 的設定與驗證。
範例 1:基礎設定與 Token 產生
# main.py
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from typing import List
from pydantic import BaseModel
app = FastAPI()
# 1. 建立 OAuth2PasswordBearer,指向 token 端點
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# 2. 假資料:使用者與對應的 scope
fake_users_db = {
"alice": {"username": "alice", "password": "secret", "scopes": ["read_items"]},
"bob": {"username": "bob", "password": "secret", "scopes": ["read_items", "write_items"]},
}
class Token(BaseModel):
access_token: str
token_type: str
@app.post("/token", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
"""
依據使用者提供的帳號密碼產生 JWT,並在 token 中寫入對應的 scopes。
"""
user = fake_users_db.get(form_data.username)
if not user or user["password"] != form_data.password:
raise HTTPException(status_code=400, detail="Incorrect username or password")
# 這裡直接回傳簡易的字串作為 token,實務上請使用 JWT
token = f"{user['username']}|{' '.join(user['scopes'])}"
return {"access_token": token, "token_type": "bearer"}
說明:此範例僅示範概念,實務上應使用
pyjwt或python-jose產生簽名的 JWT,並在 token 中放入scopes欄位。
範例 2:驗證 Scope 的依賴函式
# utils.py
from fastapi import Security, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
from typing import List
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def get_current_user(security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme)):
"""
解析 token,取得使用者名稱與其 scopes,並檢查是否包含請求所需的 scopes。
"""
# 這裡的 token 格式為 "username|scope1 scope2"
try:
username, token_scopes_str = token.split("|")
token_scopes = token_scopes_str.split()
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
)
# 檢查每一個所需的 scope 是否存在於 token 中
for scope in security_scopes.scopes:
if scope not in token_scopes:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Not enough permissions. Missing scope: {scope}",
)
return {"username": username, "scopes": token_scopes}
範例 3:在路由上使用不同的 Scopes
# main.py (續)
from utils import get_current_user, SecurityScopes, Security
@app.get("/items/", tags=["items"])
async def read_items(current_user: dict = Security(get_current_user, scopes=["read_items"])):
"""
只需要 `read_items` 權限的端點。若 token 未包含此 scope,會回傳 401。
"""
return {"msg": f"Hello {current_user['username']}, you can read items."}
@app.post("/items/", tags=["items"])
async def create_item(
current_user: dict = Security(get_current_user, scopes=["write_items"])
):
"""
需要 `write_items` 權限的端點。只有擁有此 scope 的使用者(如 bob)可以呼叫。
"""
return {"msg": f"Hello {current_user['username']}, you can create items."}
範例 4:多重 Scope 結合(AND 與 OR)
# utils.py (續)
def get_current_user_or(security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme)):
"""
OR 方式檢查:只要 token 含有任意一個需求的 scope 即可通過。
"""
username, token_scopes_str = token.split("|")
token_scopes = token_scopes_str.split()
# 若沒有任何需求 scope,直接通過
if not security_scopes.scopes:
return {"username": username, "scopes": token_scopes}
# 檢查是否至少有一個符合
if any(scope in token_scopes for scope in security_scopes.scopes):
return {"username": username, "scopes": token_scopes}
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Missing required scopes: {security_scopes.scopes}",
)
# main.py (續)
@app.get("/dashboard/", tags=["dashboard"])
async def view_dashboard(
current_user: dict = Security(get_current_user_or, scopes=["read_items", "admin"])
):
"""
只要 token 具備 `read_items` **或** `admin` 其中之一,就能存取此頁面。
"""
return {"msg": f"Welcome {current_user['username']} to the dashboard."}
範例 5:使用 JWT 並自動解碼 Scopes
# jwt_utils.py
from jose import JWTError, jwt
from datetime import datetime, timedelta
from typing import List
SECRET_KEY = "super-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def decode_access_token(token: str):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
scopes: List[str] = payload.get("scopes", [])
if username is None:
raise JWTError()
return {"username": username, "scopes": scopes}
except JWTError:
raise HTTPException(status_code=401, detail="Could not validate credentials")
# main.py (使用 JWT 版)
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jwt_utils import create_access_token, decode_access_token
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = fake_users_db.get(form_data.username)
if not user or user["password"] != form_data.password:
raise HTTPException(status_code=400, detail="Incorrect username or password")
access_token = create_access_token(
data={"sub": user["username"], "scopes": user["scopes"]}
)
return {"access_token": access_token, "token_type": "bearer"}
def get_current_user(security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme)):
token_data = decode_access_token(token)
for scope in security_scopes.scopes:
if scope not in token_data["scopes"]:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Missing scope: {scope}",
)
return token_data
重點:使用 JWT 時,
scopes直接寫入 payload,FastAPI 只需要在依賴函式中解碼並比對即可。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 忘記在 token 中寫入 scopes | 產生的 token 只包含 sub,導致授權檢查永遠失敗。 |
在產生 JWT 時,務必把 scopes 欄位加入 payload。 |
| 硬寫 scope 名稱 | 直接在程式碼寫 "read_items",日後若要變更會造成維護困難。 |
使用常數或 Enum 來統一管理 scope 名稱。 |
| 過度授權 | 為了方便,給所有 token ["*"] 或全部權限。 |
最小權限原則:只給需要的 scope,並在授權伺服器端驗證使用者的實際權限。 |
| Scope 與 Role 混用 | 把 Role 直接當作 Scope,導致權限過於粗糙。 | 先把 Role 映射成一組預設 scope,再由 FastAPI 依 scope 做細粒度控制。 |
| 未處理 token 過期 | token 過期後仍允許存取資源。 | 在 JWT 解碼時檢查 exp 欄位,或使用 fastapi.security.HTTPBearer 的 auto_error=False 讓自訂錯誤處理更彈性。 |
最佳實踐:
- 使用 Enum 管理 Scope
from enum import Enum class Scopes(str, Enum): READ_ITEMS = "read_items" WRITE_ITEMS = "write_items" ADMIN = "admin" - 把 Scope 驗證抽成可重用的依賴,如前面的
get_current_user、get_current_user_or。 - 在 OpenAPI 文件中說明每個端點所需的 scope,FastAPI 會自動產生
security區塊,讓前端開發者一目了然。 - 使用 HTTPS,確保 token 在傳輸過程中不被竊聽。
- 定期旋轉密鑰(JWT secret),並在驗證失敗時回傳明確的錯誤訊息,避免資訊泄漏。
實際應用場景
| 場景 | 需求 | 可能的 Scope 設計 |
|---|---|---|
| 部落格平台 | 讀取文章、發表評論、編輯/刪除文章 | read_posts, create_comments, edit_posts, delete_posts |
| 企業內部系統 | 員工只能查詢自己的資料,管理者可以管理全部 | self_read, self_write, admin_read, admin_write |
| IoT 裝置管理 | 裝置只能上傳感測資料,管理者可以遠端控制 | upload_metrics, device_control |
| 多租戶 SaaS | 每個租戶只能存取自己的資源 | tenant_{tenant_id}_read, tenant_{tenant_id}_write(動態產生) |
範例:在 SaaS 中,我們可以在授權伺服器根據租戶 ID 動態產生
tenant_123_read與tenant_123_write,然後在 FastAPI 路由使用scopes=["tenant_123_read"],確保不同租戶之間互不干擾。
總結
- OAuth2 Scopes 為 API 授權提供了細粒度的控制手段,是實踐最小權限原則的關鍵。
- 在 FastAPI 中,只需要透過
OAuth2PasswordBearer、Security與自訂的依賴函式,即可輕鬆完成 Scope 的驗證。 - 實務上建議使用 JWT 來攜帶
scopes,搭配 Enum 統一管理名稱,並在授權伺服器端做好角色與 Scope 的映射。 - 注意 過度授權、忘記寫入 scopes、以及 未處理 token 過期 等常見陷阱,遵循 HTTPS、密鑰輪換 等安全最佳實踐。
- 最後,將 Scope 設計與業務需求緊密結合,才能在真實專案中達到安全、彈性與易維護的平衡。
透過上述概念與範例,你已具備在 FastAPI 中使用 OAuth2 Scopes 的完整能力,接下來就可以把這套機制套用到自己的專案,為 API 打下堅實的安全基礎。祝開發順利!