FastAPI 进阶篇:从“会写接口”到“生产级架构”的完美蜕变
1. 引子:基础篇之后,还缺什么?
基础篇讲完了 FastAPI 的"骨架"——路由、参数、响应、异常处理、依赖注入。你学会了写 API,但离"能上线的 API"还有一段距离。
假设你写了一个博客系统,基础篇教你的东西能让你完成 CRUD,但现实中的问题是:
- 接口没有认证,任何人都可以删文章——需要 OAuth2 + JWT
- 每个接口都写一遍请求日志——需要 中间件
- 用户注册完要等邮件发送完才能响应——需要 后台任务
- 前端想要实时收到新评论通知——需要 WebSocket
- 改完代码不敢上线——需要 测试
这篇博客把这些问题一次性解决。

2. 核心概念解析
高级篇涉及的概念比基础篇多,但彼此关联。先花两分钟建立整体认知:
中间件(Middleware):相当于请求的"海关检查通道"。每个请求进来都要过一遍海关,出去也要再过一遍。海关可以记录日志、检查证件、测量时间。对应到代码,就是 @app.middleware('http') 装饰器。
洋葱模型:中间件的执行顺序像一个洋葱——先注册的中间件在最外层。请求"剥开"洋葱进入核心(你的业务代码),响应再从核心"穿出"洋葱。第一个注册的中间件,它的前置逻辑最先执行,后置逻辑最后执行。
OAuth2 密码流:用户拿用户名+密码换 Token,然后每次请求都带上 Token。FastAPI 的 OAuth2PasswordBearer 自动从请求头中提取 Token,如果 Token 不存在或无效,自动返回 401。
JWT(JSON Web Token):Token 的一种格式,特点是"自包含"——服务器不需要存 Session,因为用户信息和过期时间都编码在 Token 本身里。服务器只需要验证签名(用 SECRET_KEY)就能确认 Token 的真伪。
BackgroundTasks:FastAPI 内置的轻量级异步任务工具。响应优先返回,任务在后台默默执行。适合发邮件、写日志这类操作。如果任务需要可靠投递或跨进程执行,应该换成 Celery。
WebSocket:HTTP 是"你问我答"——客户端请求,服务端响应。WebSocket 是"打电话"——建立连接后,双方随时可以说话。适合聊天、实时通知、协作编辑。
Lifespan:应用的"生命周期钩子"。应用启动时执行一次(连接数据库、加载模型),关闭时执行一次(释放资源)。用 @asynccontextmanager 实现。
3. 核心体系:四个生产级能力拆解
3.1 中间件机制
中间件的本质是一个函数,接收 request 和一个 call_next 参数。
import time
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware('http')
async def add_process_time(request: Request, call_next):
"""计算每个请求的处理时间,添加到响应头"""
# 请求前:记录开始时间
start_time = time.perf_counter()
# 调用下一层(路径操作或其他中间件)
response = await call_next(request)
# 响应后:计算耗时,写入响应头
process_time = time.perf_counter() - start_time
response.headers['X-Process-Time'] = str(process_time)
return response
call_next 是理解中间件机制的关键:
- 调用
call_next(request)才会继续执行,不调用则请求被拦截 call_next必须await,因为路径操作可能是异步的call_next返回Response,你可以修改响应头或完全替换响应
洋葱模型的执行顺序:
# 后注册的中间件,前置逻辑先执行
@app.middleware('http')
async def m1(request, call_next):
print('m1 前置') # 第2步
response = await call_next(request)
print('m1 后置') # 第4步
return response
@app.middleware('http')
async def m2(request, call_next):
print('m2 前置') # 第1步(最先执行)
response = await call_next(request)
print('m2 后置') # 第5步(最后执行)
return response
# 执行顺序:m2前置 → m1前置 → 路径操作 → m1后置 → m2后置
也就是说:后注册的中间件更靠近客户端,先注册的更靠近业务逻辑。
内置中间件通过 app.add_middleware() 注册:
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.middleware.gzip import GZipMiddleware
# CORS 跨域(最常用的内置中间件)
app.add_middleware(
CORSMiddleware,
allow_origins=['http://localhost:3000', 'https://example.com'],
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
)
# 限制 Host 头,防止 Host 头注入攻击
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=['example.com', '*.example.com', 'localhost'],
)
# 响应压缩(大 JSON 响应效果明显)
app.add_middleware(GZipMiddleware, minimum_size=1000)
CORS 的一个常见坑:
allow_credentials=True时,allow_origins不能为["*"],必须明确指定源列表。这是浏览器安全规范,不是 FastAPI 的限制。
3.2 安全认证体系
安全认证是高级篇最重的部分,分三层:密码存储 → Token 签发 → 依赖封装。
第一层:密码加密
任何时候都不要明文存密码。bcrypt 是目前最常用的密码哈希库,它自动生成随机盐(salt),防止彩虹表攻击:
import bcrypt
class PwdContext:
"""密码加密和验证的工具类"""
def hash(self, password: str) -> str:
"""加密密码"""
# gensalt() 生成随机盐,轮数越高越安全(也越慢)
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
return hashed.decode('utf-8')
def verify(self, plain_password: str, hashed_password: str) -> bool:
"""验证密码是否匹配"""
return bcrypt.checkpw(
plain_password.encode('utf-8'),
hashed_password.encode('utf-8')
)
pwd_context = PwdContext()
第二层:OAuth2 密码流 + JWT
OAuth2PasswordBearer 的 tokenUrl 参数告诉客户端去哪换 Token。当客户端访问受保护路由却没有携带有效 Token 时,FastAPI 自动返回 401。
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from datetime import datetime, timedelta
# ============ 配置 ============
SECRET_KEY = 'your-secret-key-keep-it-secret' # 生产环境用环境变量
ALGORITHM = 'HS256' # 签名算法
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # Token 过期时间
# tokenUrl 指向登录接口的路径
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token')
# ============ 签发 Token ============
def create_access_token(data: dict) -> str:
"""生成 JWT Token"""
# 复制数据,避免修改原始 dict
to_encode = data.copy()
# 设置过期时间(exp 是 JWT 标准字段)
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({'exp': expire})
# 用密钥签名,生成 JWT 字符串
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
# ============ 登录接口 ============
@app.post('/token')
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
"""用户登录,返回 Token"""
# 验证用户名密码(authenticate_user 需要自己实现)
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=401, detail='用户名或密码错误')
# 签发 Token,sub 字段存储用户标识
access_token = create_access_token({'sub': user.username})
return {'access_token': access_token, 'token_type': 'bearer'}
第三层:封装为可复用的依赖
把 Token 解析逻辑封装成一个依赖函数,任何受保护路由都可以通过 Depends(get_current_user) 注入当前用户:
async def get_current_user(token: str = Depends(oauth2_scheme)):
"""从 Token 中解析当前登录用户"""
try:
# 解码 Token,验证签名和过期时间
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get('sub')
if username is None:
raise HTTPException(status_code=401, detail='无效 Token')
except JWTError:
# Token 过期、签名错误等情况
raise HTTPException(status_code=401, detail='无效 Token')
# 从数据库查询用户(get_user_by_username 需要自己实现)
user = await get_user_by_username(username)
if user is None:
raise HTTPException(status_code=401, detail='用户不存在')
return user
# 使用方式:任何需要登录的路由直接注入
@app.get('/users/me')
async def read_users_me(current_user: User = Depends(get_current_user)):
return {'username': current_user.username, 'email': current_user.email}
这样设计的优势是:认证逻辑只写一次,所有受保护路由只需要加一行 Depends(get_current_user)。
3.3 后台任务与实时通信
BackgroundTasks:轻量级异步任务
适用于"响应不需要等它完成"的操作。关键点是:BackgroundTasks.add_task() 会把任务添加到队列中,响应返回后再顺序执行这些任务。注意这里说的"后置"是指响应已经发送给客户端之后。
from fastapi import BackgroundTasks
def send_welcome_email(email: str, username: str):
"""模拟发送欢迎邮件(同步函数即可)"""
print(f'发送欢迎邮件到 {email}: 你好 {username}!')
# 实际项目中这里调用邮件服务 API
def write_audit_log(action: str, user_id: int):
"""写入审计日志"""
print(f'[AUDIT] 用户 {user_id} 执行了: {action}')
@app.post('/users/register')
async def register(
username: str,
email: str,
background_tasks: BackgroundTasks,
):
"""用户注册:响应优先返回,后台发邮件 + 写日志"""
# 注册逻辑(创建用户等)
user_id = 123 # 假设创建成功返回的 ID
# 添加后台任务(可以添加多个)
background_tasks.add_task(send_welcome_email, email, username)
background_tasks.add_task(write_audit_log, '用户注册', user_id)
# 响应立即返回,不用等邮件发送
return {'message': '注册成功', 'user_id': user_id}
BackgroundTasks 的局限性:任务存储在内存中,进程崩溃后任务丢失。不适合需要可靠投递的场景(支付回调等),这类场景应使用 Celery + Redis/RabbitMQ。
WebSocket 实时通信
WebSocket 与 HTTP 共享同一个端口,FastAPI 可以同时处理两种协议。基本通信模式是:accept → 循环 receive/send → 处理断连。
from fastapi import WebSocket, WebSocketDisconnect
# 维护所有活跃连接的列表
active_connections: list[WebSocket] = []
@app.websocket('/ws/notifications')
async def websocket_endpoint(websocket: WebSocket):
"""WebSocket 通知推送端点"""
# 接受 WebSocket 连接
await websocket.accept()
# 加入连接池
active_connections.append(websocket)
print(f'新连接加入,当前连接数: {len(active_connections)}')
try:
# 循环接收消息(保持连接活跃)
while True:
data = await websocket.receive_text()
print(f'收到客户端消息: {data}')
# 广播给所有连接的客户端
for conn in active_connections:
await conn.send_text(f'服务端广播: {data}')
except WebSocketDisconnect:
# 客户端断开时,从连接池移除
active_connections.remove(websocket)
print(f'连接断开,当前连接数: {len(active_connections)}')
WebSocket 实战要点:
- 必须在
try/except中捕获WebSocketDisconnect,否则断连后下次广播会报错- 连接池建议用
set代替list(避免重复添加,移除效率更高)- 实际项目中需要做心跳检测(ping/pong),清理僵尸连接
3.4 生命周期与工程化
Lifespan 事件
用 @asynccontextmanager 装饰一个异步生成器函数,yield 之前的代码在应用启动时执行,之后的在关闭时执行:
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用生命周期管理"""
# === 启动时执行 ===
print('应用启动中...')
await init_database() # 连接数据库
await load_ml_model() # 加载机器学习模型
print('应用启动完成')
yield # 应用运行期间
# === 关闭时执行 ===
print('应用关闭中...')
await close_database() # 关闭数据库连接
print('应用已关闭')
app = FastAPI(lifespan=lifespan)
通用响应封装
让所有接口返回统一的 JSON 格式,前端处理起来更方便:
from typing import TypeVar, Generic
from pydantic import BaseModel
# 泛型类型变量,让返回的数据类型可以动态指定
T = TypeVar('T')
class Result(BaseModel, Generic[T]):
"""统一响应格式"""
code: int = 200
message: str = 'success'
data: T | None = None
@classmethod
def success(cls, data: T | None = None):
"""成功响应"""
return cls(code=200, message='success', data=data)
@classmethod
def error(cls, code: int, message: str):
"""失败响应"""
return cls(code=code, message=message, data=None)
# 使用示例
@app.get('/users/{user_id}', response_model=Result[UserOut])
async def get_user(user_id: int):
user = await find_user(user_id)
if not user:
return Result.error(404, '用户不存在')
return Result.success(user)
测试编写
TestClient 基于 httpx,同步调用 FastAPI 应用,无需启动服务器:
from fastapi.testclient import TestClient
from app.main import app
# 创建测试客户端,全程不需要启动 Uvicorn
client = TestClient(app)
def test_create_book():
"""测试创建图书接口"""
response = client.post('/books/', json={
'title': 'Python 从入门到实践',
'author': 'Eric Matthes',
'price': 89.0,
})
# 验证状态码
assert response.status_code == 201
# 验证返回数据
data = response.json()
assert data['title'] == 'Python 从入门到实践'
assert 'id' in data
def test_get_nonexistent_book():
"""测试获取不存在的图书"""
response = client.get('/books/99999')
assert response.status_code == 404
assert response.json()['detail'] == '书籍不存在'
依赖覆盖是 FastAPI 测试的最大优势——用 Mock 数据库替换真实数据库,不污染数据:
# 生产环境的依赖
async def get_db():
async with AsyncSessionLocal() as session:
yield session
# 测试环境的 Mock 依赖
async def override_get_db():
"""测试环境使用内存数据库"""
async with MemoryAsyncSession() as session:
yield session
# 替换依赖
app.dependency_overrides[get_db] = override_get_db
# 现在 TestClient 的所有请求都使用内存数据库
def test_create_book():
response = client.post('/books/', json={'title': '测试图书', 'price': 99.0})
assert response.status_code == 201
4. 实战代码演示:带认证的实时通知系统
下面用一个完整的"用户通知系统"串联所有高级篇知识点。这个系统做的事情很简单:
- 用户注册 / 登录(JWT 认证)
- 用户创建一条通知
- 系统通过 WebSocket 实时推送给所有在线用户
- 后台任务记录审计日志
- 中间件统计请求耗时
- 测试覆盖核心接口
4.1 完整代码
"""
FastAPI 高级特性实战:用户通知系统
演示:中间件 + JWT 认证 + WebSocket + 后台任务 + 测试
"""
import time
import bcrypt
from datetime import datetime, timedelta
from typing import Annotated
from fastapi import (
FastAPI, Depends, HTTPException, WebSocket,
WebSocketDisconnect, BackgroundTasks, Request, status,
)
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from fastapi.responses import JSONResponse
from jose import JWTError, jwt
from pydantic import BaseModel
# ============ 配置 ============
SECRET_KEY = 'dev-secret-key-change-in-production'
ALGORITHM = 'HS256'
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# ============ 数据模型 ============
class UserCreate(BaseModel):
username: str
password: str
email: str
class UserOut(BaseModel):
"""响应模型:不暴露密码"""
username: str
email: str
class Notification(BaseModel):
title: str
content: str
# ============ 密码工具 ============
class PwdContext:
def hash(self, password: str) -> str:
salt = bcrypt.gensalt()
return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
def verify(self, plain: str, hashed: str) -> bool:
return bcrypt.checkpw(plain.encode('utf-8'), hashed.encode('utf-8'))
pwd_context = PwdContext()
# ============ 模拟数据库 ============
fake_users_db: dict[str, dict] = {}
fake_notifications_db: list[dict] = []
next_user_id = 1
# ============ 应用初始化 ============
app = FastAPI()
# OAuth2 方案:tokenUrl 指向登录接口
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='/token')
# ============ ③ 中间件:请求耗时统计 ============
@app.middleware('http')
async def add_process_time(request: Request, call_next):
"""每个请求都会经过这里,记录处理时间"""
start = time.perf_counter()
response = await call_next(request)
elapsed = time.perf_counter() - start
response.headers['X-Process-Time'] = f'{elapsed:.4f}s'
return response
# ============ 认证工具函数 ============
def create_access_token(data: dict) -> str:
"""签发 JWT Token"""
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({'exp': expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
"""从 Token 解析当前用户(可复用的依赖)"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get('sub')
if username is None or username not in fake_users_db:
raise HTTPException(status_code=401, detail='无效 Token')
except JWTError:
raise HTTPException(status_code=401, detail='Token 已过期或无效')
return fake_users_db[username]
# ============ ② WebSocket 连接管理 ============
active_ws_connections: set[WebSocket] = set()
@app.websocket('/ws/notifications')
async def websocket_notifications(websocket: WebSocket):
"""WebSocket 端点:连接后实时接收通知推送"""
await websocket.accept()
active_ws_connections.add(websocket)
try:
# 保持连接,等待客户端断开
while True:
await websocket.receive_text()
except WebSocketDisconnect:
active_ws_connections.discard(websocket)
async def broadcast_notification(title: str, content: str):
"""向所有 WebSocket 客户端广播通知"""
message = f'新通知: {title} - {content}'
# 遍历连接池,断连的会自动捕获 WebSocketDisconnect
for conn in list(active_ws_connections):
try:
await conn.send_text(message)
except WebSocketDisconnect:
active_ws_connections.discard(conn)
# ============ ① 后台任务函数 ============
def write_audit_log(action: str, username: str):
"""后台任务:写审计日志(响应返回后执行)"""
print(f'[AUDIT] {datetime.now()}: {username} 执行了 {action}')
# ============ API 路由 ============
@app.post('/register', response_model=UserOut)
async def register(user_in: UserCreate, background_tasks: BackgroundTasks):
"""用户注册(带后台任务)"""
global next_user_id
if user_in.username in fake_users_db:
raise HTTPException(status_code=400, detail='用户名已存在')
# 创建用户,密码加密存储
fake_users_db[user_in.username] = {
'id': next_user_id,
'username': user_in.username,
'email': user_in.email,
'hashed_password': pwd_context.hash(user_in.password),
}
next_user_id += 1
# 添加后台任务:响应返回后写审计日志
background_tasks.add_task(write_audit_log, '用户注册', user_in.username)
return {'username': user_in.username, 'email': user_in.email}
@app.post('/token')
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
"""用户登录:验证身份 → 签发 Token"""
user = fake_users_db.get(form_data.username)
if not user or not pwd_context.verify(form_data.password, user['hashed_password']):
raise HTTPException(status_code=401, detail='用户名或密码错误')
token = create_access_token({'sub': form_data.username})
return {'access_token': token, 'token_type': 'bearer'}
@app.post('/notifications')
async def create_notification(
notif: Notification,
background_tasks: BackgroundTasks,
current_user: Annotated[dict, Depends(get_current_user)],
):
"""创建通知(需要登录)→ 写入数据库 → WebSocket 广播"""
# 保存通知
notification_record = {
'id': len(fake_notifications_db) + 1,
'title': notif.title,
'content': notif.content,
'created_by': current_user['username'],
'created_at': datetime.now().isoformat(),
}
fake_notifications_db.append(notification_record)
# 后台任务:审计日志
background_tasks.add_task(
write_audit_log, '创建通知', current_user['username']
)
# WebSocket 广播(异步执行,不阻塞响应)
await broadcast_notification(notif.title, notif.content)
return notification_record
@app.get('/notifications')
async def list_notifications(
current_user: Annotated[dict, Depends(get_current_user)],
):
"""获取通知列表(需要登录)"""
return fake_notifications_db
# ============ 测试代码 ============
from fastapi.testclient import TestClient
client = TestClient(app)
def test_full_flow():
"""测试完整流程:注册 → 登录 → 创建通知 → 获取通知"""
# 1. 注册
resp = client.post('/register', json={
'username': 'testuser',
'password': 'pass123',
'email': 'test@example.com',
})
assert resp.status_code == 200
assert resp.json()['username'] == 'testuser'
# 验证响应模型中不包含 password
assert 'password' not in resp.json()
# 2. 登录获取 Token
resp = client.post('/token', data={
'username': 'testuser',
'password': 'pass123',
})
assert resp.status_code == 200
token = resp.json()['access_token']
assert token is not None
# 3. 用 Token 创建通知(放在 Authorization header 中)
resp = client.post(
'/notifications',
json={'title': '欢迎', 'content': '欢迎使用通知系统'},
headers={'Authorization': f'Bearer {token}'},
)
assert resp.status_code == 200
assert resp.json()['title'] == '欢迎'
# 4. 获取通知列表
resp = client.get(
'/notifications',
headers={'Authorization': f'Bearer {token}'},
)
assert resp.status_code == 200
assert len(resp.json()) == 1
# 5. 验证未携带 Token 的请求被拒绝
resp = client.get('/notifications')
assert resp.status_code == 401
# 6. 验证响应头中有处理时间(中间件注入的)
assert 'X-Process-Time' in resp.headers
4.2 运行结果说明
启动服务后:
# 注册用户
curl -X POST http://localhost:8000/register \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "secret123", "email": "alice@test.com"}'
# 返回: {"username": "alice", "email": "alice@test.com"}
# 登录获取 Token
curl -X POST http://localhost:8000/token \
-d "username=alice&password=secret123"
# 返回: {"access_token": "eyJ...", "token_type": "bearer"}
# 用 Token 创建通知
curl -X POST http://localhost:8000/notifications \
-H "Authorization: Bearer eyJ..." \
-H "Content-Type: application/json" \
-d '{"title": "系统通知", "content": "欢迎回来"}'
# 未携带 Token 的请求
curl http://localhost:8000/notifications
# 返回: 401 Unauthorized
运行测试:
# 直接运行测试函数
python -c "test_full_flow()"
# 看到 [AUDIT] 日志输出在后台任务中执行
5. 避坑指南 / 最佳实践
5.1 JWT 的常见安全陷阱
# ❌ 错误:密钥硬编码在代码中
SECRET_KEY = 'hardcoded-secret'
# ✓ 正确:从环境变量读取
import os
SECRET_KEY = os.getenv('JWT_SECRET_KEY')
if not SECRET_KEY:
raise RuntimeError('JWT_SECRET_KEY 环境变量未设置')
另外注意:JWT 的 payload 是 Base64 编码而非加密的,任何拿到 Token 的人都可以解码查看 payload 中的内容。不要在 payload 中存放敏感信息(密码、身份证号等)。
5.2 中间件注册顺序
CORS 中间件应该注册在最外层(最靠近客户端),这样跨域检查在所有其他逻辑之前执行:
# ✓ 正确顺序:CORS 最先注册
app.add_middleware(CORSMiddleware, allow_origins=['*'], ...)
@app.middleware('http')
async def logging_middleware(request, call_next):
...
@app.middleware('http')
async def auth_middleware(request, call_next):
...
如果 CORS 中间件在内层,跨域请求的 OPTIONS 预检请求可能会被内层中间件拦截,导致跨域失败。
5.3 BackgroundTasks 的适用边界
| 场景 | 用 BackgroundTasks | 用 Celery |
|---|---|---|
| 发邮件 | ✓ 可以 | ✓ 也可以 |
| 写日志 | ✓ 推荐 | 杀鸡用牛刀 |
| 清理临时文件 | ✓ 可以 | 没必要 |
| 支付回调通知 | ❌ 进程崩溃会丢失 | ✓ 必须 |
| 定时任务(每天凌晨跑报表) | ❌ 不支持 | ✓ 必须 |
| CPU 密集型计算(图像处理) | ❌ 阻塞事件循环 | ✓ 独立 Worker |
一句话原则:BackgroundTasks 适合"丢了也不心疼"的轻量任务。重要任务交给 Celery。
5.4 WebSocket 连接管理
# ✓ 正确:使用 set 去重,discard 安全移除
active_connections: set[WebSocket] = set()
active_connections.add(websocket)
active_connections.discard(websocket) # 不存在也不会报错
# ❌ 错误:广播时遍历的同时修改集合
for conn in active_connections:
if broken(conn):
active_connections.remove(conn) # RuntimeError: Set changed size during iteration
# ✓ 正确:遍历副本
for conn in list(active_connections):
if broken(conn):
active_connections.discard(conn)
5.5 测试的依赖覆盖
app.dependency_overrides 是一个字典,调用 TestClient 前设置好覆盖。不同测试之间要清理状态:
def test_something():
# 设置覆盖
app.dependency_overrides[get_db] = override_get_db
# 执行测试
response = client.get('/items/')
# 清理覆盖(防止影响其他测试)
app.dependency_overrides.clear()
# 断言
assert response.status_code == 200
也可以用 pytest.fixture 实现更优雅的生命周期管理。
5.6 官方文档
- FastAPI Middleware
- FastAPI CORS
- FastAPI Security (OAuth2 + JWT)
- FastAPI Background Tasks
- FastAPI WebSockets
- FastAPI Lifespan Events
- FastAPI Testing
6. 总结
下表总结了"生产环境刚需"与"FastAPI 对应方案"的映射关系:
| 生产需求 | FastAPI 方案 | 一句话要点 |
|---|---|---|
| 请求日志 / 耗时统计 | @app.middleware('http') | 不调用 call_next 则请求被拦截 |
| 跨域访问 | CORSMiddleware | allow_credentials=True 时 origins 不能为 ["*"] |
| 密码安全存储 | bcrypt | 自动加盐,永远不要存明文 |
| 用户认证 | OAuth2PasswordBearer + JWT | Token 自包含,服务端无状态 |
| 轻量后台任务 | BackgroundTasks | 响应返回后执行,丢失不心疼 |
| 实时消息推送 | WebSocket | 维护连接池,处理断连 |
| 应用启动/关闭 | Lifespan | yield 前启动,yield 后关闭 |
| 自动化测试 | TestClient + 依赖覆盖 | 不需要启动服务器 |
| 统一响应格式 | Result[T] 泛型封装 | 前端不用猜响应格式 |
基础篇教你写 API,高级篇教你写"能上线的 API"。两者结合,再加上后面会讲的数据库操作,一个完整的 FastAPI 后端服务的拼图就齐了。
下篇预告:FastAPI 数据库篇——SQLAlchemy 2.0 异步 ORM 实战。讲清楚 Engine、Session、模型定义、CRUD 操作、事务管理和 Alembic 迁移,以及如何在 FastAPI 中优雅地管理数据库连接。

348

被折叠的 条评论
为什么被折叠?



