1. FastAPI 的 Depends 和 Middleware 实现鉴权拦截
- 依赖注入(
Depends) - 中间件(
Middleware)
1.1. 依赖注入(Depends)------ 细粒度、路由级鉴权
原理
Depends声明一个函数(或可调用对象)作为依赖项,FastAPI 会在路由执行前自动解析并注入它的返回值。- 依赖项可以嵌套依赖(依赖树),且默认每次请求 重新计算(除非用
@lru_cache等缓存)。 - 支持
async依赖,也支持yield依赖用于资源管理(如数据库会话)。
鉴权实现
最常见的做法是定义 get_current_user 依赖,它从请求头提取 Token,验证后返回用户信息。
py
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
app = FastAPI()
security = HTTPBearer()
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
token = credentials.credentials
try:
payload = jwt.decode(token, "SECRET_KEY", algorithms=["HS256"])
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(status_code=401, detail="Invalid token")
return {"user_id": user_id, "role": payload.get("role")}
except jwt.PyJWTError:
raise HTTPException(status_code=401, detail="Invalid token")
@app.get("/protected")
def protected_route(user=Depends(verify_token)):
return {"message": f"Hello user {user['user_id']}"}
优势
- 精确控制:可针对不同路由使用不同认证策略(如某些路由可选认证)。
- 自动注入:依赖项可注入到其他依赖中,形成组合。
- 测试友好:可轻松 mock 依赖。
深入机制
- FastAPI 会构建依赖图,并缓存依赖实例(默认在请求范围内复用)。
- 若依赖项是
async,会在异步上下文中执行。
1.2. 中间件(Middleware)------ 全局、统一鉴权
原理
中间件是 WSGI/ASGI 层面的拦截器,在请求到达路由之前和响应返回客户端之前执行。它采用"洋葱模型":请求穿过中间件链,再穿过中间件返回。
鉴权实现
中间件适合统一拦截所有请求(或路径前缀),进行全局认证,但无法灵活区分不同路由的认证需求。
py
from fastapi import Request, Response, HTTPException
from starlette.middleware.base import BaseHTTPMiddleware
import jwt
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# 放行健康检查路径
if request.url.path in ("/health", "/docs", "/openapi.json"):
return await call_next(request)
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return Response("Unauthorized", status_code=401)
token = auth_header.split(" ")[1]
try:
payload = jwt.decode(token, "SECRET_KEY", algorithms=["HS256"])
request.state.user = payload # 将用户信息存入请求状态
except jwt.PyJWTError:
return Response("Invalid token", status_code=401)
response = await call_next(request)
return response
app.add_middleware(AuthMiddleware)
优势
- 全局生效,无需在每个路由重复认证代码。
- 可统一处理响应头、日志等。
深入机制
- 中间件是 ASGI 应用的一部分,
dispatch方法接收request,调用call_next传递请求给下一个中间件或路由。 - 中间件可以修改请求(添加属性)或提前返回响应。
3. 对比与选择
| 特性 | Depends 依赖注入 | 中间件 |
|---|---|---|
| 粒度 | 路由级,可精细控制 | 全局,统一拦截 |
| 易用性 | 依赖项可复用,易于组合 | 需要编写类,配置稍复杂 |
| 异常处理 | 在依赖中抛 HTTPException,自动转为 4xx 响应 |
需手动返回 Response 或 JSONResponse |
| 访问请求体 | 依赖可访问请求体(需 Body) |
中间件访问请求体需额外处理(流式) |
| 性能 | 每次请求解析依赖树,轻量 | 每次请求经过中间件链,性能略有损耗 |
| 最佳实践 | 推荐用于业务逻辑的鉴权、授权 | 推荐用于全局日志、限流、CORS、健康检查绕过 |
4. 高级:结合两者实现"通用+精细"鉴权
一种常见模式是:
- 中间件 :负责解析 Token,将用户信息存入
request.state(但若失败只记录错误,不直接拦截)。 - 依赖项 :从
request.state读取用户信息,并执行具体的权限校验(如角色检查)。
这样既实现了全局 Token 解析(减少重复代码),又保留了路由级别的权限控制。
python
# 依赖项
def get_current_user(request: Request):
user = getattr(request.state, "user", None)
if user is None:
raise HTTPException(401, "Unauthorized")
return user
@app.get("/admin")
def admin_route(user=Depends(get_current_user)):
if user.get("role") != "admin":
raise HTTPException(403, "Not enough permissions")
return {"status": "ok"}
5. 依赖注入的高级特性
5.1 类依赖
py
class CommonHeaders:
def __init__(self, user_agent: str = Header(...)):
self.user_agent = user_agent
@app.get("/items")
def read_items(headers: CommonHeaders = Depends()):
return {"ua": headers.user_agent}
5.2 Yield 依赖(资源管理)
py
async def get_db():
db = await connect_db()
try:
yield db
finally:
await db.close()
@app.get("/users")
def get_users(db=Depends(get_db)):
return db.query("SELECT * FROM users")
5.3 全局依赖
在 app 或 APIRouter 上添加 dependencies 列表,会对该路由下所有端点生效。
6. 中间件的注意事项
- 请求体访问 :中间件读取请求体可能需要
request.stream(),但会消耗流,导致路由无法读取。解决方案:将请求体缓存或使用request.body()(注意大小)。 - 异常处理 :在中间件中抛出
HTTPException不会被 FastAPI 的异常处理器捕获,需手动处理并返回Response。 - 执行顺序:中间件按添加顺序执行(先进后出)。
7. 完整鉴权示例(Middleware + Depends + JWT)
使用中间件提取 Token 并存入 request.state ,再用 Depends 函数从 request.state 获取用户并执行权限校验,这样既保持全局统一,又不失路由灵活性。
py
import jwt
from fastapi import FastAPI, Depends, HTTPException, Request, status
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from starlette.middleware.base import BaseHTTPMiddleware
from typing import Optional
# ---------- 配置 ----------
SECRET_KEY = "your-secret-key" # 生产环境应从环境变量读取
ALGORITHM = "HS256"
# ---------- 数据模型 ----------
class User(BaseModel):
user_id: str
role: str # "admin" 或 "user"
# ---------- 中间件:提取 Token 并解码 ----------
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# 1. 放行不需要认证的路径(可选)
if request.url.path in ("/health", "/docs", "/openapi.json"):
return await call_next(request)
# 2. 提取 Authorization 头
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Missing or invalid Authorization header"},
headers={"WWW-Authenticate": "Bearer"},
)
token = auth_header.split(" ")[1]
# 3. 解码 JWT
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user = User(user_id=payload.get("sub"), role=payload.get("role", "user"))
request.state.user = user # 存入 request.state
except jwt.PyJWTError:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Invalid token"},
headers={"WWW-Authenticate": "Bearer"},
)
# 4. 继续处理请求
response = await call_next(request)
return response
# ---------- 依赖:从 request.state 获取当前用户 ----------
def get_current_user(request: Request) -> User:
user = getattr(request.state, "user", None)
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
return user
# 依赖:仅允许 admin 角色
def require_admin(current_user: User = Depends(get_current_user)) -> User:
if current_user.role != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions",
)
return current_user
# ---------- FastAPI 应用 ----------
app = FastAPI(title="JWT Auth Demo")
# 添加中间件
app.add_middleware(AuthMiddleware)
# ---------- 路由 ----------
@app.get("/health")
async def health():
return {"status": "ok"}
@app.get("/me")
async def get_my_info(current_user: User = Depends(get_current_user)):
"""获取当前登录用户信息(任何有效 token 均可)"""
return {"user_id": current_user.user_id, "role": current_user.role}
@app.get("/admin")
async def admin_only(current_user: User = Depends(require_admin)):
"""仅 admin 角色可访问"""
return {"message": f"Welcome admin {current_user.user_id}"}
2. 数据库SQL
2.1 mysql 数据库
MySQL 由三层组成:
- 连接层:客户端和服务器端建立连接,客户端发送 SQL至服务器端;
- SQL 层:对 SQL 语句进行查询处理;
- 存储引擎层:与数据库文件打交道,负责数据的存储和读取。
设计原则 - 三少一多
- 数据表的个数越少越好
- 数据表中的字段个数越少越好
- 数据表中联合主键的字段个数越少越好
- 使用主键和外键越多越好
2.2. 全文搜索引擎 Elasticsearch
Elasticsearch(简称 ES)是一个开源的分布式搜索与分析引擎,基于 Apache Lucene 构建。它擅长对海量文本做近实时的全文检索,搜索引擎是数据库检索中的重要应用。搜索引擎的优势在于采用了全文搜索的技术,核心原理是"倒排索引"。
py
from elasticsearch import Elasticsearch
import os
# 连接本地 ES
es = Elasticsearch("http://localhost:9200")
# 或连接 Elastic Cloud
# es = Elasticsearch(
# cloud_id=os.environ['ELASTIC_CLOUD_ID'],
# api_key=os.environ['ELASTIC_API_KEY']
# )
# 验证连接
print(es.info())
# 索引文档
doc = {
"name": "iPhone 15 Pro",
"brand": "Apple",
"price": 7999.00,
"in_stock": True
}
es.index(index="products", id=1, document=doc)
# 搜索文档
resp = es.search(
index="products",
query={"match": {"name": "iPhone"}}
)
for hit in resp['hits']['hits']:
print(hit['_source'])
# 聚合分析
resp = es.search(
index="products",
size=0,
aggs={"by_brand": {"terms": {"field": "brand.keyword"}}}
)
print(resp['aggregations'])