在之前的两篇教程中,我们学习了 FastAPI 的基础知识和核心概念,包括路由、依赖项、Pydantic 模型、表单与文件上传、中间件等。现在,是时候深入探索 FastAPI 的高级特性,这些特性将帮助你构建更复杂、更安全、更易维护的应用程序。本篇教程将涵盖 WebSocket、数据库集成(SQLAlchemy)、安全认证(OAuth2 JWT)、应用结构优化、部署策略等内容,并通过实战示例展示如何将这些特性整合到一个完整的项目中。
1. WebSocket 支持
FastAPI 原生支持 WebSocket,可以轻松实现实时双向通信,例如聊天应用、实时通知、游戏等。
1.1 基础 WebSocket 路由
使用 @app.websocket 装饰器定义一个 WebSocket 端点。在路径操作函数中,你需要接收一个 WebSocket 对象,并手动处理连接、接收和发送消息。
python
from fastapi import FastAPI, WebSocket
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Message text was: {data}")
await websocket.accept()接受连接。websocket.receive_text()接收文本消息,还有receive_bytes()、receive_json()等。websocket.send_text()发送文本消息。
1.2 广播消息
实际应用中,你可能需要将消息广播给多个连接的客户端。可以维护一个连接列表。
python
class ConnectionManager:
def __init__(self):
self.active_connections: list[WebSocket] = []
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def send_personal_message(self, message: str, websocket: WebSocket):
await websocket.send_text(message)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: int):
await manager.connect(websocket)
try:
while True:
data = await websocket.receive_text()
await manager.send_personal_message(f"You wrote: {data}", websocket)
await manager.broadcast(f"Client #{client_id} says: {data}")
except WebSocketDisconnect:
manager.disconnect(websocket)
await manager.broadcast(f"Client #{client_id} left the chat")
WebSocket 与 HTTP 可以共存于同一个应用,非常灵活。
2. 数据库集成(以 SQLAlchemy 为例)
大多数 Web 应用需要持久化数据。FastAPI 不强制使用特定的数据库库,但与 SQLAlchemy(异步或同步)配合非常顺畅。推荐使用异步 SQLAlchemy(sqlalchemy.ext.asyncio)以获得更好的性能。
2.1 安装依赖
bash
pip install sqlalchemy databases asyncpg # asyncpg 是 PostgreSQL 异步驱动
如果使用同步 SQLAlchemy,可以省略 databases 和异步驱动。
2.2 定义模型和数据库连接
python
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "postgresql://user:pass@localhost/dbname"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
class Item(Base):
__tablename__ = "items"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
description = Column(String)
2.3 创建依赖项获取数据库会话
python
from fastapi import Depends
from sqlalchemy.orm import Session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
然后在路径操作中使用 Depends(get_db) 获取会话。
python
@app.post("/items/")
def create_item(item: ItemCreate, db: Session = Depends(get_db)):
db_item = Item(name=item.name, description=item.description)
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item
2.4 异步 SQLAlchemy
对于异步操作,使用 create_async_engine 和 AsyncSession。
python
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
ASYNC_DB_URL = "postgresql+asyncpg://user:pass@localhost/dbname"
async_engine = create_async_engine(ASYNC_DB_URL)
AsyncSessionLocal = sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False)
async def get_async_db():
async with AsyncSessionLocal() as session:
yield session
然后在异步路径操作中使用 await。
python
@app.post("/items/")
async def create_item(item: ItemCreate, db: AsyncSession = Depends(get_async_db)):
db_item = Item(name=item.name, description=item.description)
db.add(db_item)
await db.commit()
await db.refresh(db_item)
return db_item
3. 安全性与认证(OAuth2 JWT)
FastAPI 提供了多种安全工具,包括 HTTP Basic、OAuth2 密码流(带 JWT 令牌)等。最常用的是 OAuth2 密码流 + JWT。
3.1 安装 PyJWT 和 Passlib
bash
pip install python-jose[cryptography] passlib[bcrypt]
python-jose用于生成和验证 JWT。passlib用于密码哈希(使用 Bcrypt)。
3.2 密码哈希和验证
python
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
3.3 创建 JWT 令牌
python
from datetime import datetime, timedelta
from jose import JWTError, jwt
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def create_access_token(data: dict, expires_delta: timedelta = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
3.4 OAuth2 密码流依赖项
使用 OAuth2PasswordBearer 获取令牌。
python
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(fake_db, form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=400, detail="Incorrect username or password")
access_token = create_access_token(data={"sub": user.username})
return {"access_token": access_token, "token_type": "bearer"}
3.5 获取当前用户
python
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = get_user(fake_db, username)
if user is None:
raise credentials_exception
return user
然后在需要保护的路由中依赖 get_current_user。
python
@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_user)):
return current_user
4. 应用结构优化(APIRouter)
随着项目增长,将所有路由放在一个文件中会变得混乱。FastAPI 提供了 APIRouter 来模块化路由。
4.1 使用 APIRouter
在 routers/items.py 中:
python
from fastapi import APIRouter, Depends
router = APIRouter(prefix="/items", tags=["items"])
@router.get("/")
async def read_items():
return [{"name": "Foo"}]
@router.get("/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
4.2 包含路由
在主 app 中:
python
from fastapi import FastAPI
from routers import items, users
app = FastAPI()
app.include_router(items.router)
app.include_router(users.router)
你还可以为每个路由器设置前缀、标签、依赖项等,提高代码组织性。
5. 部署策略
FastAPI 应用可以部署在各种环境中,常见的方式是使用 Uvicorn 配合 Gunicorn(作为进程管理器),或者使用 Docker。
5.1 使用 Gunicorn + Uvicorn 工作进程
安装 gunicorn 和 uvicorn:
bash
pip install gunicorn uvicorn
启动命令:
bash
gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker
-w 4:启动 4 个工作进程。-k uvicorn.workers.UvicornWorker:指定使用 Uvicorn 工作进程。
5.2 Docker 部署
编写 Dockerfile:
dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
构建并运行:
bash
docker build -t myapp .
docker run -p 80:80 myapp
在生产环境中,通常还会使用 Nginx 作为反向代理,以及使用环境变量管理配置。
6. 实战:整合所有特性的小项目
下面我们通过一个简单的"实时聊天 + 用户认证"项目来演示如何将上述高级特性整合在一起。
6.1 项目结构
chat_app/
├── main.py
├── routers/
│ ├── auth.py
│ ├── chat.py
│ └── items.py
├── models.py
├── schemas.py
├── database.py
├── dependencies.py
├── security.py
└── requirements.txt
6.2 主要文件内容
database.py:创建数据库引擎和会话。models.py:定义 SQLAlchemy 模型。schemas.py:定义 Pydantic 模型(用于请求/响应)。security.py:密码哈希、JWT 处理。dependencies.py:公共依赖项(如 get_db, get_current_user)。routers/auth.py:注册、登录、令牌获取。routers/chat.py:WebSocket 聊天室。routers/items.py:示例 CRUD 操作(受保护)。main.py:创建应用,包含所有路由器。
6.3 代码片段示例
security.py
python
from passlib.context import CryptContext
from jose import JWTError, jwt
from datetime import datetime, timedelta
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def verify_password(plain, hashed):
return pwd_context.verify(plain, hashed)
def get_password_hash(password):
return pwd_context.hash(password)
def create_access_token(data: dict):
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)
dependencies.py
python
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from sqlalchemy.orm import Session
from . import models, database, security
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
def get_db():
db = database.SessionLocal()
try:
yield db
finally:
db.close()
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, security.SECRET_KEY, algorithms=[security.ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = db.query(models.User).filter(models.User.username == username).first()
if user is None:
raise credentials_exception
return user
routers/auth.py
python
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from .. import schemas, models, dependencies, security
router = APIRouter(prefix="/auth", tags=["authentication"])
@router.post("/register", response_model=schemas.User)
def register(user: schemas.UserCreate, db: Session = Depends(dependencies.get_db)):
db_user = db.query(models.User).filter(models.User.username == user.username).first()
if db_user:
raise HTTPException(status_code=400, detail="Username already registered")
hashed = security.get_password_hash(user.password)
new_user = models.User(username=user.username, hashed_password=hashed)
db.add(new_user)
db.commit()
db.refresh(new_user)
return new_user
@router.post("/token")
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(dependencies.get_db)):
user = db.query(models.User).filter(models.User.username == form_data.username).first()
if not user or not security.verify_password(form_data.password, user.hashed_password):
raise HTTPException(status_code=400, detail="Incorrect username or password")
access_token = security.create_access_token(data={"sub": user.username})
return {"access_token": access_token, "token_type": "bearer"}
routers/chat.py
python
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from ..dependencies import get_current_user # 注意:WebSocket 不能轻易使用依赖项,需手动验证
from ..security import decode_token
router = APIRouter(prefix="/chat", tags=["chat"])
class ConnectionManager:
def __init__(self):
self.active_connections = {}
async def connect(self, websocket: WebSocket, user_id: int):
await websocket.accept()
self.active_connections[user_id] = websocket
def disconnect(self, user_id: int):
self.active_connections.pop(user_id, None)
async def send_personal(self, message: str, user_id: int):
if user_id in self.active_connections:
await self.active_connections[user_id].send_text(message)
async def broadcast(self, message: str, exclude_user: int = None):
for uid, conn in self.active_connections.items():
if uid != exclude_user:
await conn.send_text(message)
manager = ConnectionManager()
@router.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
# 手动从查询参数获取 token 并验证
token = websocket.query_params.get("token")
if not token:
await websocket.close(code=1008)
return
try:
payload = decode_token(token) # 假设 decode_token 会抛出异常如果无效
user_id = payload.get("sub")
except:
await websocket.close(code=1008)
return
await manager.connect(websocket, user_id)
try:
while True:
data = await websocket.receive_text()
await manager.broadcast(f"User {user_id}: {data}", exclude_user=user_id)
except WebSocketDisconnect:
manager.disconnect(user_id)
await manager.broadcast(f"User {user_id} left the chat")
main.py
python
from fastapi import FastAPI
from .routers import auth, chat, items
app = FastAPI()
app.include_router(auth.router)
app.include_router(chat.router)
app.include_router(items.router)
@app.get("/")
def root():
return {"message": "Chat App API"}
6.4 运行与测试
- 启动数据库(如 PostgreSQL)。
- 运行
uvicorn chat_app.main:app --reload。 - 访问
/docs查看自动生成的 API 文档。 - 通过
/auth/register创建用户。 - 通过
/auth/token获取令牌。 - 使用 WebSocket 客户端(如浏览器 JavaScript 或 Postman)连接到
/chat/ws?token=<your_token>,并测试聊天功能。
7. 性能优化建议
- 使用异步数据库驱动 :如
asyncpg配合 SQLAlchemy 异步。 - 缓存:使用 Redis 缓存频繁访问的数据。
- Gzip 压缩 :启用
GZipMiddleware减少响应大小。 - 连接池:合理配置数据库连接池大小。
- 限制请求体大小 :使用
max_request_size等参数防止恶意大请求。 - 后台任务:将耗时操作放入后台任务。
8. 总结
通过本教程,我们深入探索了 FastAPI 的高级特性:
- WebSocket 实现实时通信。
- 数据库集成(SQLAlchemy)进行持久化。
- 安全认证(OAuth2 JWT)保护端点。
- 应用结构优化(APIRouter)使代码可维护。
- 部署策略(Gunicorn、Docker)将应用投入生产。
- 实战项目整合了所有特性,展示了如何构建一个功能完整的聊天应用。
FastAPI 的强大之处不仅在于其性能,还在于其清晰的设计和丰富的功能。掌握这些高级特性后,你已经具备了构建复杂、安全、可扩展的现代 Web 应用的能力。希望这三篇教程能成为你探索 FastAPI 世界的坚实基础。Happy coding!