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 %}