Python 后端核心知识

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 响应 需手动返回 ResponseJSONResponse
访问请求体 依赖可访问请求体(需 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 全局依赖

appAPIRouter 上添加 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 由三层组成:

  1. 连接层:客户端和服务器端建立连接,客户端发送 SQL至服务器端;
  2. SQL 层:对 SQL 语句进行查询处理;
  3. 存储引擎层:与数据库文件打交道,负责数据的存储和读取。

设计原则 - 三少一多

  • 数据表的个数越少越好
  • 数据表中的字段个数越少越好
  • 数据表中联合主键的字段个数越少越好
  • 使用主键和外键越多越好

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'])
相关推荐
RobinDevNotes3 分钟前
用 Profile 揪出大模型训练性能瓶颈
python·性能优化
李航198310 分钟前
用 DeepDraw 几何引擎开发建筑设计软件(六):创建画线工具
python·软件构建
AI 编程助手GPT10 分钟前
Python 备份 SQLite:为什么复制了 .db,恢复后还是少数据?
人工智能·python·ai·chatgpt
狗都不学爬虫_24 分钟前
AI逆向 - 天御无感点击验证(补+纯)
爬虫·python·网络爬虫
529宝宝起名网1 小时前
用 Python 开发历史名字查询与起名灵感工具:从古籍人物数据库到名字文化故事生成
开发语言·前端·python
用户298698530141 小时前
Python 将 HTML 转换为 Word 文档的实践指南
python·html·api
维克兜率天1 小时前
【维克】模块3总结:从一行空数据,到一个能跑的模型
python·深度学习·算法
飞Link1 小时前
定积分理论与 Python 仿真完全指南
python·算法
小静AI工程实验室1 小时前
Python 爬虫翻页为何重复、漏数据?SQLite 复现 OFFSET、复合游标与快照的 8 项检查
爬虫·python·sqlite
夜雪一千2 小时前
如何使用Python实现音频转文本(ASR语音识别)
python