authorization === ###### tags: `Python / FastAPI` ###### tags: `Python`, `PyPi`, `backend`, `fastapi` <br> [TOC] <br> ## 討論 ### 如何用 curl 進行驗證 basic auth - ### Question ```python= security = HTTPBasic() def get_openai_permission( credentials: HTTPBasicCredentials = Depends(security), ) -> str: """ Get openai permission. :param credentials: credentials. :returns: credentials username :raises HTTPException: if call to something fails... """ correct_username = secrets.compare_digest( credentials.username, settings.openapi_dev_account, ) correct_password = secrets.compare_digest( credentials.password, settings.openapi_dev_password, ) ``` 我要如何用 curl 打 request,取出 `/api/docs` ? - ### gpt-4 若要使用 `curl` 請求 HTTP 基礎認證保護的 FastAPI `/api/docs` 端點,你需要將使用者名稱和密碼以 Base64 編碼的方式傳遞給 `Authorization` 頭部。以下是一個 `curl` 請求的示例: ```bash curl -X 'GET' \ 'http://localhost:8000/api/docs' \ -H 'accept: text/html' \ -H 'Authorization: Basic <base64_encoded_credentials>' ``` 在上面的命令中,你需要將 `<base64_encoded_credentials>` 替換為你的使用者名稱和密碼的 Base64 編碼。編碼的格式是 `username:password`。你可以在大多數的 Unix 系統(包括 macOS)和 Windows 的命令行中使用 `echo` 和 `base64` 命令來生成這個編碼: ```bash echo -n "your_username:your_password" | base64 ``` - `-n`: do not output the trailing newline 將 `your_username` 和 `your_password` 替換為你的憑證。命令的輸出將是 Base64 編碼的字符串,你需要將這個字符串插入到 curl 命令的 Authorization 頭部中。 如果你使用的是 Windows,則可以在 PowerShell 中使用以下命令來生成 Base64 編碼的憑證: ```powershell [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("your_username:your_password")) ``` 在 `curl` 命令中使用生成的 Base64 編碼後,就能對 `/api/docs` 端點發出請求並獲得回應。如果認證正確且你有訪問該端點的權限,你將能夠取得 API 文件。 <br> <hr> <br> ### FastAPI 中使用自訂 Middleware 驗證 vs `starlette.authentication` 差異比較 - ### Question 自訂 Middleware 驗證(透過 `openapi_schema` 加入 `BearerAuth` 驗證), 跟使用 starlette.authentication 機制差別在哪裡? - ### gpt-4o (2025/03/31) > FastAPI 中使用自訂 Middleware 驗證 vs `starlette.authentication` 差異比較 這份紀錄說明了兩種在 FastAPI 中實作 API Token 驗證的方式: 1. 使用自訂 middleware(你目前的實作) 2. 使用 Starlette 提供的 `AuthenticationMiddleware` + `AuthenticationBackend` --- ### ✅ 簡單比較表 | 項目 | 自訂 middleware 驗證方式 | Starlette `AuthenticationMiddleware` | |------|---------------------------|--------------------------------------| | 驗證方式 | 自訂 middleware 檢查 `Authorization` header | 實作 `AuthenticationBackend` 類別 | | 擴展性 | 需自行控制驗證流程與例外情境 | 提供標準介面與流程 | | 使用者資訊 | 無 `request.user` 可用 | 提供 `request.user`, `request.auth` | | 使用範圍 | 全域 middleware 判斷所有路由 | 可搭配權限控制、使用者群組等設計 | | 路由控制 | 需手動跳過特定路由驗證(如 `/docs`) | 可在 `backend` 或 route 裡統一處理 | | 錯誤回應 | 自行回傳 401/403 錯誤訊息 | 可統一處理或自訂錯誤格式 | --- ### 🧠 機制說明 #### 1. 你目前的自訂 Middleware 驗證方式 **優點** - 快速開發,簡單直覺。 - 不需額外套件或介面實作。 **缺點** - 所有驗證邏輯都必須手動撰寫與維護。 - 無法使用 `request.user` 或權限控制機制。 - 不利擴充與測試,例如加入 OAuth、JWT、RBAC 等。 --- #### 2. 使用 Starlette 的 AuthenticationMiddleware Starlette 提供更結構化的驗證流程: ##### ✅ 建立自訂驗證後端 `AuthenticationBackend` 使用者需自行實作: ```python= from starlette.authentication import ( AuthenticationBackend, AuthCredentials, SimpleUser ) class BearerTokenBackend(AuthenticationBackend): async def authenticate(self, conn): auth = conn.headers.get("Authorization") if not auth or not auth.lower().startswith("bearer "): return token = auth[7:].strip() if token != "expected_token": return return AuthCredentials(["authenticated"]), SimpleUser("client") ``` --- ##### ✅ 將驗證後端掛載至 Middleware ```python= from starlette.middleware.authentication import AuthenticationMiddleware app.add_middleware( AuthenticationMiddleware, backend=BearerTokenBackend() ) ``` --- ##### ✅ 在路由中使用驗證結果 ```python= @app.get("/secure-data") async def secure_data(request: Request): if not request.user.is_authenticated: raise HTTPException(status_code=403, detail="Unauthorized") return {"message": f"Hello {request.user.display_name}"} ``` --- ### 🔁 適用情境 **使用 Starlette Auth 的建議情境:** - 需要多種登入機制(Token, Cookie, OAuth 等)。 - 想在路由中存取 `user`, `auth` 等資訊。 - 需要支援 RBAC、ACL、群組/權限控管。 - 希望將驗證模組化並進行單元測試。 --- ### ✅ 混合使用建議 如果目前只需要簡單的 Bearer token 驗證,自訂 middleware 是夠用的。 **但若預期功能成長,建議轉向使用 `AuthenticationMiddleware`,因為:** - 更好維護與測試 - 統一驗證邏輯 - 支援更多進階安全機制 --- ### 🚀 想進一步優化? 如果你希望我幫你 refactor 成基於 Starlette 的驗證方式, 並保留現有的 token 驗證邏輯(例如從 K8s 拿 token 等), 我可以幫你提供完整的範本 + 測試方式。 <br> <hr> <br> ### 使用 Starlette AuthenticationMiddleware 重構 Bearer Token 驗證 - ### Question (承上)進一步優化 - ### gpt-4o (2025/03/31) > ⭐ 使用 Starlette AuthenticationMiddleware 重構 Bearer Token 驗證 本文說明如何將現有 FastAPI + middleware 驗證邏輯,改寫成使用 Starlette 的 `AuthenticationMiddleware` + 自訂 `AuthenticationBackend`。此設計可支援 `request.user`、更彈性的 RBAC 與可測試架構。 --- ### 📦 專案結構建議 ``` app/ ├── main.py ├── auth/ │ ├── backend.py 👈 Bearer token 驗證邏輯 │ └── models.py 👈 User/Auth 資料結構(可選) ├── utils/ │ └── kubernetes_helper2.py └── config/ └── settings.py ``` --- ### 🔐 `backend.py` — 自訂 BearerTokenAuthBackend ```python= # app/auth/backend.py from typing import Optional, Tuple from starlette.authentication import ( AuthCredentials, AuthenticationBackend, BaseUser, UnauthenticatedUser ) from starlette.requests import HTTPConnection from app.utils.kubernetes_helper2 import k8s_helper2 class SimpleUser(BaseUser): """Represents an authenticated user with a simple ID.""" def __init__(self, username: str): self.username = username @property def is_authenticated(self) -> bool: return True @property def display_name(self) -> str: return self.username class BearerTokenAuthBackend(AuthenticationBackend): """ Authentication backend that validates Bearer tokens in the Authorization header. """ async def authenticate( self, conn: HTTPConnection ) -> Optional[Tuple[AuthCredentials, BaseUser]]: auth_header = conn.headers.get("Authorization") if not auth_header or not auth_header.lower().startswith("bearer "): return None # Unauthenticated token = auth_header[7:].strip() # Check against authorized tokens valid_tokens = k8s_helper2.get_client_api_tokens() if token not in valid_tokens: return None # Invalid token # Assign roles/scopes as needed return AuthCredentials(["authenticated"]), SimpleUser("client_user") ``` --- ### ⚙️ `main.py` — FastAPI 初始化與 middleware 設定 ```python= # app/main.py from fastapi import FastAPI, Request, Depends, HTTPException from fastapi.responses import JSONResponse from starlette.middleware.authentication import AuthenticationMiddleware from starlette.authentication import requires from app.auth.backend import BearerTokenAuthBackend app = FastAPI() # Add authentication middleware app.add_middleware( AuthenticationMiddleware, backend=BearerTokenAuthBackend() ) @app.get("/api/public") async def public_endpoint(): return {"message": "No auth needed"} @app.get("/api/secure") @requires("authenticated") async def secure_endpoint(request: Request): return { "message": f"Hello, {request.user.display_name}", "user": str(request.user) } ``` --- ### ✅ 驗證行為說明 | Endpoint | 驗證行為 | 回應 | |------------------|------------------------------|-------------------------------| | `/api/public` | 不需驗證 | 回傳成功 | | `/api/secure` | 需帶上 Bearer token | 無效或缺少 → 403 Forbidden | | `/api/secure` | 有效 token → 顯示使用者資訊 | OK | --- ### ✨ 優勢 - ✅ `request.user` 可用於 handler 中 - ✅ 使用 `@requires("authenticated")` 做權限控管 - ✅ 自訂 `SimpleUser` 可擴充 scopes, roles, group 等概念 - ✅ 驗證邏輯可測試、模組化、易維護 --- ### 🧪 測試範例 ``` curl -X GET http://localhost:8000/api/secure \ -H "Authorization: Bearer <your_token>" ``` --- ### 🧭 後續建議擴充方向 1. ✅ 加入 **RBAC 設計**(支援角色與權限判斷) 2. ✅ 自動設定 **OpenAPI securitySchemes** 3. ✅ 根據 user 限制可存取的資源範圍(user-level scoping) --- 如需上述功能範本,歡迎提出,我可以繼續協助補上實作 😎 <br> <hr> <br> ### FastAPI Authentication + RBAC Integration Guide - ### Question (承上)一併補上 - ### gpt-4o (2025/03/31) > 🔐 FastAPI Authentication + RBAC Integration Guide 這份文件說明如何將 FastAPI 與 Starlette AuthenticationMiddleware 整合,並加入: | 功能 | 說明 | |-----|-----| | ✅ Bearer Token 驗證 | 使用者身份驗證 | | ✅ RBAC 角色權限控制 | 使用者身份授權,根據 `admin`,`user`,`guest` 等給予不同的存取權限 | | ✅ 自訂 OpenAPI security schema | | | ✅ 使用者綁定資源查詢功能 | 根據 `request.user` 給予限制或授權 | --- ### 📦 專案結構建議 ``` app/ ├── auth/ │ ├── backend.py 👈 Bearer token 驗證與 RBAC │ ├── models.py 👈 User model with roles │ └── dependencies.py 👈 權限驗證器 (Depends 使用) ├── main.py ├── routes/ │ └── client.py 👈 客戶端相關 API ├── utils/ │ └── kubernetes_helper2.py ├── config/ │ └── settings.py └── openapi/ └── custom_openapi.py 👈 自訂 OpenAPI security ``` --- ### 🔐 `models.py` — 使用者模型(支援角色) ```python= # app/auth/models.py from starlette.authentication import BaseUser class RoleUser(BaseUser): def __init__(self, username: str, roles: list[str]): self.username = username self.roles = roles @property def is_authenticated(self) -> bool: return True @property def display_name(self) -> str: return self.username def has_role(self, role: str) -> bool: return role in self.roles ``` --- ### 🧠 `backend.py` — 驗證邏輯 + RBAC 判斷 ```python= # app/auth/backend.py from typing import Optional, Tuple from starlette.authentication import ( AuthCredentials, AuthenticationBackend ) from starlette.requests import HTTPConnection from app.utils.kubernetes_helper2 import k8s_helper2 from app.auth.models import RoleUser class BearerTokenAuthBackend(AuthenticationBackend): async def authenticate( self, conn: HTTPConnection ) -> Optional[Tuple[AuthCredentials, RoleUser]]: auth_header = conn.headers.get("Authorization") if not auth_header or not auth_header.lower().startswith("bearer "): return None token = auth_header[7:].strip() valid_tokens = k8s_helper2.get_client_api_tokens() if token not in valid_tokens: return None # 模擬從 token 判斷角色:abc123-admin → admin role = "admin" if token.endswith("-admin") else "user" user = RoleUser(username="client_user", roles=[role]) return AuthCredentials(["authenticated"]), user ``` --- ### 🔒 `dependencies.py` — 權限限制函式 (for Depends) ```python= # app/auth/dependencies.py from fastapi import Depends, HTTPException, Request, status def require_role(role: str): async def _role_dependency(request: Request): user = request.user if not user.is_authenticated or not user.has_role(role): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=f"Requires '{role}' role" ) return user return _role_dependency ``` --- ### 📄 `custom_openapi.py` — 加入 Bearer Token 安全標記 ```python= # app/openapi/custom_openapi.py from fastapi.openapi.utils import get_openapi from fastapi import FastAPI def custom_openapi(app: FastAPI): if app.openapi_schema: return app.openapi_schema openapi_schema = get_openapi( title=app.title, version=app.version, description=app.description, routes=app.routes, ) openapi_schema["components"]["securitySchemes"] = { "BearerAuth": { "type": "http", "scheme": "bearer" } } for path in openapi_schema["paths"].values(): for method in path.values(): method["security"] = [{"BearerAuth": []}] app.openapi_schema = openapi_schema return app.openapi_schema ``` --- ### 🚀 `main.py` — 整合所有設定 ```python= # app/main.py from fastapi import FastAPI from starlette.middleware.authentication import AuthenticationMiddleware from app.auth.backend import BearerTokenAuthBackend from app.openapi.custom_openapi import custom_openapi from app.routes import client app = FastAPI() app.add_middleware( AuthenticationMiddleware, backend=BearerTokenAuthBackend() ) app.openapi = lambda: custom_openapi(app) app.include_router(client.router) ``` --- ### 👥 `client.py` — 加入使用者驗證與角色控制 ```python= # app/routes/client.py from fastapi import APIRouter, Request, Depends from app.auth.dependencies import require_role router = APIRouter(prefix="/api/client") @router.get("/me") async def get_self(request: Request): return {"user": request.user.display_name} @router.get("/admin-only") async def admin_only( request: Request, user=Depends(require_role("admin")) ): return {"message": f"Hello admin {user.display_name}"} ``` --- ### 🧪 測試 API #### ✅ 一般 user 可用 ``` curl -H "Authorization: Bearer abc123-user" \ http://localhost:8000/api/client/me ``` #### ❌ user 權限存取 admin-only → 403 ``` curl -H "Authorization: Bearer abc123-user" \ http://localhost:8000/api/client/admin-only ``` #### ✅ admin 權限可以存取 ``` curl -H "Authorization: Bearer abc123-admin" \ http://localhost:8000/api/client/admin-only ``` --- ### 🔧 可擴充方向 - ➕ 支援多種角色與細緻權限,如:`["admin", "editor", "viewer"]` - 🔑 加入 OAuth2 / JWT 驗證 - 🗂️ 使用 DB 儲存使用者與角色 - ✅ 使用 `request.user` 控制每位用戶可查詢的資料範圍 --- 如需擴充 JWT、OAuth、角色綁定資料庫等,歡迎加上,我可以協助進一步改寫。 備註關鍵字: - `from fastapi.security import OAuth2PasswordBearer` <br> {%hackmd vaaMgNRPS4KGJDSFG0ZE0w %}