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'])
相关推荐
denggun123451 小时前
yield
前端·数据库·python
zx_741484812 小时前
【Python 入门】面向对象基础:类、对象、成员变量与构造方法
开发语言·python
招财小梗3 小时前
沈阳商贸公司AI企业服务优势几何?
大数据·人工智能·python
ZISHU_9874 小时前
用 TLabel 给 SynTouch BioTac 数据做语义标注:从原始信号到结构化标注
开发语言·人工智能·python·数据·机器人触觉
m0_617493945 小时前
SSLError [ASN1: NOT_ENOUGH_DATA] 问题排查与解决指南
python·ssl
whcyhhh5 小时前
头歌实践教学平台:数据科学与大数据技术导论(七上)
大数据·数据库·python
ServBay5 小时前
AI 工程师必备的 9 个 Python 库,从数据验证到模型优化
后端·python·ai编程
AC赳赳老秦6 小时前
企业级合规审计体系:用 OpenClaw 落地采集全链路留痕,自动生成合规审计报告
java·python·django·beautifulsoup·php·deepseek·openclaw
IPdodo_6 小时前
代理 IP 服务商 SLA 怎么验?7 项指标与 Python 探测脚本实战
运维·python·网络协议·网络安全·代理ip