本文 AI 產出,尚未審核
FastAPI 課程 – 安全性單元
主題:密碼雜湊(bcrypt、passlib)
簡介
在 Web 應用程式中,使用者密碼的安全存放是最基本也是最重要的防線。即使資料庫被入侵,若密碼是以明文或弱雜湊方式保存,攻擊者可以輕易還原出原始密碼,進一步造成帳號竊取、身份冒用等嚴重後果。
FastAPI 作為一個現代化的非同步 API 框架,內建支援多種安全機制,其中密碼雜湊是最常見的需求。本文將以 bcrypt 為例,說明如何透過 passlib 這個高階的雜湊函式庫,安全、簡潔地在 FastAPI 中完成密碼的加密、驗證與管理,適合剛接觸 FastAPI 的初學者,也能提供給中階開發者作為最佳實踐的參考。
核心概念
1. 為什麼選擇 bcrypt?
- 計算成本可調:bcrypt 內建「工作因子」(cost factor),可以隨硬體提升而調高,使暴力破解成本隨之上升。
- 內建鹽值 (salt):每一次雜湊都會自動產生唯一的鹽值,避免相同密碼產生相同雜湊。
- 抗 GPU 暴力:相較於 MD5、SHA1 等快速雜湊演算法,bcrypt 設計上較不適合平行化運算,減少 GPU 暴力破解的威脅。
2. passlib 的角色
passlib 是 Python 生態系中最完整的密碼雜湊套件,提供:
- 統一的 API(
hash,verify,needs_update) - 多種演算法的支援(bcrypt、argon2、pbkdf2 等)
- 版本管理與自動升級檢測
- 兼容舊有雜湊格式的遷移工具
在 FastAPI 中使用 passlib,可以把雜湊邏輯抽離成獨立的 服務層,保持路由 (router) 的簡潔與可測試性。
3. 基本流程
- 註冊:使用者提供明文密碼 →
passlib產生 bcrypt 雜湊 → 儲存雜湊於資料庫。 - 登入:使用者提交密碼 → 取出對應的雜湊 →
passlib.verify比對,成功則回傳 JWT 或 Session。 - 升級雜湊:若演算法或工作因子變更,
needs_update可偵測舊雜湊,需要時重新雜湊並更新資料庫。
程式碼範例
3.1 安裝相依套件
pip install fastapi[all] passlib[bcrypt] python-multipart
passlib[bcrypt]會同時安裝bcrypt套件,確保雜湊運算可用。
3.2 建立 PasswordHasher
# utils/security.py
from passlib.context import CryptContext
# 設定 bcrypt,rounds=12 是常見的安全預設值
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto", bcrypt__rounds=12)
def hash_password(password: str) -> str:
"""
將明文密碼雜湊成 bcrypt 字串。
"""
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
比對明文密碼與雜湊值,回傳 True/False。
"""
return pwd_context.verify(plain_password, hashed_password)
def needs_rehash(hashed_password: str) -> bool:
"""
判斷現有雜湊是否需要升級(例如 rounds 提升)。
"""
return pwd_context.needs_update(hashed_password)
3.3 FastAPI 註冊與登入路由
# main.py
from fastapi import FastAPI, HTTPException, Depends, status
from pydantic import BaseModel, EmailStr
from utils.security import hash_password, verify_password, needs_rehash
from typing import Dict
app = FastAPI()
# 模擬資料庫(實務上請使用 ORM)
fake_user_db: Dict[str, Dict] = {}
class UserCreate(BaseModel):
email: EmailStr
password: str
class UserLogin(BaseModel):
email: EmailStr
password: str
@app.post("/register", status_code=status.HTTP_201_CREATED)
def register(user: UserCreate):
if user.email in fake_user_db:
raise HTTPException(status_code=400, detail="Email already registered")
hashed = hash_password(user.password)
fake_user_db[user.email] = {"email": user.email, "hashed_password": hashed}
return {"msg": "User created successfully"}
@app.post("/login")
def login(user: UserLogin):
db_user = fake_user_db.get(user.email)
if not db_user:
raise HTTPException(status_code=400, detail="Invalid credentials")
if not verify_password(user.password, db_user["hashed_password"]):
raise HTTPException(status_code=400, detail="Invalid credentials")
# 若雜湊過時,重新雜湊並寫回資料庫
if needs_rehash(db_user["hashed_password"]):
db_user["hashed_password"] = hash_password(user.password)
# 這裡示範回傳簡易 token,實務上建議使用 JWT
return {"access_token": "fake-token", "token_type": "bearer"}
重點說明
hash_password只在註冊或雜湊升級時呼叫,永遠不要在驗證時重新雜湊。needs_rehash讓我們在 演算法升級 時自動更新使用者的雜湊,無需強迫使用者更改密碼。
3.4 使用不同演算法的設定(進階)
有時候會需要同時支援 argon2 或 pbkdf2_sha256,只要在 CryptContext 中列出即可:
# utils/security.py (進階版)
pwd_context = CryptContext(
schemes=["argon2", "bcrypt"],
deprecated="auto",
# argon2 參數範例
argon2__time_cost=2,
argon2__memory_cost=102400,
argon2__parallelism=8,
# bcrypt 預設 12 rounds
bcrypt__rounds=12,
)
# 呼叫方式保持不變
3.5 單元測試範例
# tests/test_security.py
from utils.security import hash_password, verify_password, needs_rehash
def test_hash_and_verify():
pwd = "StrongP@ssw0rd!"
hashed = hash_password(pwd)
assert verify_password(pwd, hashed) is True
assert verify_password("WrongPwd", hashed) is False
def test_needs_rehash():
# 使用較低的 rounds 產生舊雜湊
low_context = CryptContext(schemes=["bcrypt"], bcrypt__rounds=8)
old_hash = low_context.hash("test")
# 以目前的 12 rounds 判斷應該需要升級
assert needs_rehash(old_hash) is True
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 解決方案 |
|---|---|---|
| 直接儲存明文密碼 | 資料外洩即失去所有使用者信任 | 必須 使用 passlib 產生雜湊後再寫入資料庫 |
| 忽略鹽值 (salt) 的概念 | 相同密碼產生相同雜湊,易被彩虹表破解 | bcrypt 內建鹽值,不要自行 生成或重用鹽 |
雜湊成本過低 (rounds 太小) |
暴力破解速度過快 | 建議 bcrypt__rounds >= 12,根據硬體每年調整一次 |
在驗證時使用 hash 重建雜湊 |
每次驗證都產生不同雜湊,導致驗證失敗 | 使用 pwd_context.verify,不要 用 hash 再比對 |
| 未檢查雜湊升級需求 | 老舊雜湊仍被使用,安全性下降 | 登入成功後呼叫 needs_rehash,必要時重新雜湊並寫回 |
| 把雜湊寫入日誌或回傳給前端 | 泄漏雜湊資訊,給予攻擊者線索 | 永遠 只回傳成功/失敗訊息,絕不回傳雜湊字串 |
最佳實踐
- 統一管理:將所有雜湊相關函式封裝於
utils/security.py,保持路由層乾淨。 - 環境變數:將工作因子 (
bcrypt__rounds) 以環境變數形式設定,方便在不同環境調整。 - 例外處理:使用
try/except捕捉passlib.exc.UnknownHashError,防止惡意提交不合法雜湊。 - 密碼政策:在前端加強密碼複雜度檢查,後端再以
pydantic驗證最小長度與字元類型。 - 定期審計:使用工具(如
sqlmap、bandit)掃描資料庫與程式碼,確保沒有明文密碼或硬編碼的鹽值。
實際應用場景
使用者註冊與登入
- 透過上述範例,完成最基本的註冊、登入流程,配合 JWT 實作完整的認證系統。
第三方 OAuth 與本地帳號混合
- 即使使用 Google、GitHub 登入,也建議為每個使用者產生本地的「虛擬密碼」或「一次性密碼」雜湊,以便未來支援本地密碼變更。
密碼重設流程
- 產生一次性 token(如 JWT)寄送至使用者 email,使用者點擊連結後提供新密碼,再以
hash_password更新雜湊。
- 產生一次性 token(如 JWT)寄送至使用者 email,使用者點擊連結後提供新密碼,再以
多租戶 SaaS 系統
- 每個租戶的使用者資料庫分表或加上租戶 ID 作為額外的「pepper」(系統全局的鹽值) 再雜湊,提高跨租戶的安全隔離。
API 金鑰與服務帳號
- 雖然不是使用者密碼,但同樣可以使用
passlib產生雜湊的 API 金鑰,避免金鑰以明文存於資料庫。
- 雖然不是使用者密碼,但同樣可以使用
總結
- 密碼雜湊是防止資料外洩的第一道防線,選擇 bcrypt 搭配 passlib 能提供可調整的計算成本與自動鹽值管理。
- 透過
CryptContext的統一介面,我們可以輕鬆在 FastAPI 中完成 註冊、登入、雜湊升級 等全流程。 - 避免常見的陷阱(明文儲存、錯誤的驗證方式、過低的工作因子),並遵循 最佳實踐(環境變數、例外處理、密碼政策)即可打造安全且可維護的認證系統。
- 本文提供的範例與架構可以直接套用於實務專案,無論是簡單的單機應用,或是大型的多租戶 SaaS,都能確保使用者密碼的安全性。
祝開發順利,讓你的 FastAPI 應用在 安全 與 效能 之間取得最佳平衡! 🚀