Skip to main content
今日目标:普通框架写出的代码往往是一坨”意大利面条”,所有逻辑纠缠在一起。今天我们要学习 FastAPI 的独门绝技 —— 依赖注入 (Depends),学会如何像搭积木一样复用逻辑(如鉴权、数据库连接);同时掌握 Middleware,实现全局层面的流量拦截。今天不只是写代码,而是要彻底理解 “依赖注入如何解耦代码”“中间件和依赖的区别” 以及 “如何设计可复用的依赖函数”

学习内容 (30 mins)

在开始写代码前,先搞懂这些核心概念,否则后面的代码你会看得云里雾里。
什么是依赖注入?依赖注入是一种设计模式,将依赖关系从代码内部移到外部,通过参数传递的方式提供依赖。为什么需要依赖注入?
  • 代码复用:将通用逻辑(如鉴权、数据库连接)封装成函数,多个接口复用
  • 解耦代码:业务逻辑和横切关注点(cross-cutting concerns)分离
  • 易于测试:可以轻松替换依赖,便于单元测试
  • 自动执行:FastAPI 自动执行依赖函数,无需手动调用
传统方式 vs 依赖注入传统方式
# 每个接口都要写一遍鉴权逻辑
@app.get("/items/")
def read_items():
    token = request.headers.get("x-token")
    if token != "secret":
        return {"error": "Invalid token"}, 401
    # 业务逻辑...

@app.get("/users/")
def read_users():
    token = request.headers.get("x-token")
    if token != "secret":
        return {"error": "Invalid token"}, 401
    # 业务逻辑...
依赖注入方式
# 定义一次,到处复用
def verify_token(x_token: str = Header(...)):
    if x_token != "secret":
        raise HTTPException(401, "Invalid token")
    return x_token

@app.get("/items/", dependencies=[Depends(verify_token)])
def read_items():
    # 业务逻辑(无需写鉴权代码)
    pass

@app.get("/users/", dependencies=[Depends(verify_token)])
def read_users():
    # 业务逻辑(无需写鉴权代码)
    pass
Depends 的工作原理
  1. FastAPI 发现路由函数有 Depends(verify_token)
  2. 在执行路由函数前,先执行 verify_token 函数
  3. 如果 verify_token 抛出异常,路由函数不会执行,直接返回错误
  4. 如果 verify_token 成功,将返回值传递给路由函数(如果需要)
什么是中间件?中间件是在请求处理前后执行的函数,可以拦截所有请求和响应。为什么需要中间件?
  • 全局处理:对所有请求执行相同的操作(如日志记录、CORS 处理)
  • 请求拦截:在请求到达路由前进行处理
  • 响应修改:在响应返回前添加头部、修改内容
中间件 vs 依赖注入
  • 依赖注入 (Depends)
    • 作用于特定的路由函数
    • 适合业务强相关的逻辑(如:用户必须登录)
    • 可以传递返回值给路由函数
  • 中间件 (Middleware)
    • 作用于全局所有请求
    • 适合通用的横切关注点(如:记录请求耗时、CORS 处理)
    • 不能直接传递数据给路由函数
类比
  • 中间件:小区保安(谁进小区都得看一眼)
  • 依赖注入:门禁卡(只有进自家单元门才刷卡)
中间件的执行顺序
  1. 请求进入 → 中间件 1 → 中间件 2 → … → 路由函数
  2. 路由函数执行
  3. 响应返回 → 中间件 2 → 中间件 1 → 客户端
常见中间件应用
  • CORS:处理跨域请求
  • 日志记录:记录每个请求的 URL、方法、耗时
  • 请求验证:验证请求格式、大小限制
  • 响应压缩:压缩响应内容

代码任务 (90 mins)

1

环境准备

确保虚拟环境已激活:
# 确保虚拟环境已激活
source .venv/bin/activate

# FastAPI 和 Uvicorn 应该已经安装(Day 15)
# 如果没有,执行:pip install fastapi uvicorn
2

任务 A:编写通用鉴权依赖

创建一个可复用的鉴权依赖函数。任务分解
  1. 创建依赖函数,从 Header 中读取 Token
  2. 验证 Token 是否有效
  3. 如果无效,抛出异常
#!/usr/bin/env python3
"""
Day 17 - 依赖注入示例
演示如何创建可复用的依赖函数
"""

from fastapi import Header, HTTPException
from typing import Optional

# ========== 依赖函数:验证 Token ==========
# 这是一个普通的异步函数
# 但它的参数 x_token 声明了来源于 Header,这很关键!
# Header(...) 表示这个参数必须从 HTTP Header 中获取
async def verify_token(x_token: str = Header(...)):
    """
    验证请求 Token
    
    参数:
        x_token: 从 Header 中获取的 Token(必须提供)
    
    返回:
        str: 验证通过的 Token
    
    抛出:
        HTTPException: Token 无效时抛出 400 错误
    """
    # 简单的 Token 验证逻辑(实际应该查询数据库或验证 JWT)
    if x_token != "fake-secret":
        # 如果 Token 对不上,直接抛出 400 错误
        # 这个异常会被 FastAPI 捕获,直接中断请求
        # 路由函数不会被执行
        raise HTTPException(
            status_code=400, 
            detail="Invalid X-Token header"
        )
    
    # 如果验证通过,返回 Token(虽然下面的路由可能不想用它)
    # 主要是为了通过验证,让路由函数能够执行
    return x_token

# ========== 可选依赖:获取当前用户 ==========
# 这个依赖依赖于 verify_token,形成依赖链
async def get_current_user(x_token: str = Header(...)):
    """
    获取当前用户信息(依赖于 Token 验证)
    
    参数:
        x_token: 从 Header 中获取的 Token
    
    返回:
        dict: 用户信息
    """
    # 先验证 Token
    if x_token != "fake-secret":
        raise HTTPException(status_code=400, detail="Invalid token")
    
    # 模拟根据 Token 查询用户信息
    # 实际应该从数据库或缓存中查询
    return {
        "id": 1,
        "username": "admin",
        "email": "admin@example.com"
    }
代码解释
  • Header(...):从 HTTP Header 中获取参数
  • HTTPException:抛出 HTTP 异常,FastAPI 自动转换为错误响应
  • 依赖链get_current_user 可以依赖 verify_token,形成依赖链
验证步骤
  1. 检查文件语法是否正确
  2. 尝试导入:python -c "from 17_deps import verify_token; print('OK')"
3

任务 B:编写耗时统计中间件

创建一个中间件,统计每个请求的处理时间。
#!/usr/bin/env python3
"""
Day 17 - 依赖注入与中间件实战
演示如何使用 Depends 和 Middleware
"""

import time
from fastapi import FastAPI, Depends, Request
from fastapi.middleware.cors import CORSMiddleware
from 17_deps import verify_token, get_current_user

# 初始化 FastAPI 应用
app = FastAPI(
    title="Dependency Injection & Middleware Demo",
    description="演示依赖注入和中间件的使用",
    version="1.0.0"
)

# ========== 1. 全局中间件:配置 CORS (跨域) ==========
# 这是 Web 开发必配的,否则前端 Vue/React 请求会被浏览器拦截
# CORS (Cross-Origin Resource Sharing) 允许不同域名的前端访问 API
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 允许任何来源(生产环境应该指定具体域名!)
    allow_credentials=True,  # 允许携带 Cookie
    allow_methods=["*"],  # 允许所有 HTTP 方法
    allow_headers=["*"],  # 允许所有 Header
)

# ========== 2. 自定义中间件:统计耗时 ==========
# @app.middleware("http") 表示这是一个 HTTP 中间件
# 中间件会在每个请求前后执行
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    """
    统计请求处理时间并添加到 Response Header
    
    参数:
        request: 进来的请求对象
        call_next: 把请求交给下一个环节处理的函数
    
    返回:
        Response: 添加了耗时信息的响应
    """
    # 1. 记下开始时间
    start_time = time.time()
    
    # 2. 等待业务逻辑处理完(这里可能要很久)
    # call_next(request) 会调用路由函数,并返回响应
    response = await call_next(request)
    
    # 3. 算出耗时(秒)
    process_time = time.time() - start_time
    
    # 4. 把耗时悄悄塞到 Response Header 里返回给前端
    # 前端可以通过 Response Headers 查看处理时间
    response.headers["X-Process-Time"] = str(process_time)
    
    return response

# ========== 3. 路由实战 ==========

# 场景 A: 公开接口(不需要 Token)
@app.get("/")
async def public_api():
    """
    公开接口:任何人都可以访问
    """
    return {"msg": "Everyone can see me"}

# 场景 B: 保护接口(需要 Token)
# dependencies=[Depends(verify_token)]: 
# 在执行 read_items 前,FastAPI 会先去跑 verify_token 函数
# 如果 verify_token 报错,read_items 根本不会执行
@app.get("/items/", dependencies=[Depends(verify_token)])
async def read_items():
    """
    保护接口:需要 Token 才能访问
    
    依赖:
        verify_token: 验证请求 Header 中的 x-token
    """
    return [
        {"item": "Secret Data A"},
        {"item": "Secret Data B"}
    ]

# 场景 C: 使用依赖的返回值
# 如果依赖函数返回了值,可以通过参数接收
@app.get("/me")
async def get_current_user_info(current_user: dict = Depends(get_current_user)):
    """
    获取当前用户信息
    
    依赖:
        get_current_user: 验证 Token 并返回用户信息
    
    参数:
        current_user: 从依赖函数获取的用户信息
    """
    return {
        "message": "Your profile",
        "user": current_user
    }

# 场景 D: 多个依赖
# 可以同时使用多个依赖
@app.get("/admin/", dependencies=[Depends(verify_token), Depends(get_current_user)])
async def admin_only():
    """
    管理员接口:需要 Token 和用户信息
    
    依赖:
        verify_token: 验证 Token
        get_current_user: 获取用户信息
    """
    return {"message": "Admin area", "status": "restricted"}
代码解释
  • CORSMiddleware:处理跨域请求,前端开发必需
  • @app.middleware("http"):定义 HTTP 中间件
  • call_next(request):调用下一个处理环节(路由函数)
  • dependencies=[Depends(...)]:在路由级别使用依赖
  • current_user: dict = Depends(...):接收依赖函数的返回值
运行脚本
# 启动服务器
uvicorn 17_main:app --reload
验证步骤
  1. 测试中间件
    • 访问 http://localhost:8000/
    • 打开浏览器开发者工具 (F12) -> Network
    • 点击请求 -> 查看 Response Headers
    • 应该能看到 x-process-time: 0.000xxxx
  2. 测试依赖注入
    • 直接访问 http://localhost:8000/items/ -> 会报错 (Missing Header)
    • 打开 Swagger UI (/docs)
    • 点击 /items/ -> Try it out
    • 在 Header 参数栏输入 x-token: fake-secret
    • 执行 -> 成功拿到数据
  3. 测试依赖返回值
    • 访问 http://localhost:8000/me
    • 在 Header 中添加 x-token: fake-secret
    • 应该能看到用户信息
常见错误
  • 422 Validation Error - Header 参数缺失或格式错误,检查 Header 名称和值
  • 400 Invalid X-Token header - Token 验证失败,检查 Token 值是否正确
  • CORS error - 跨域请求被阻止,检查 CORS 中间件配置

拓展任务 (30 mins)

挑战 1:数据库连接依赖

任务:创建一个数据库连接依赖,自动为每个请求提供数据库会话。提示
async def get_db():
    async with AsyncSessionLocal() as session:
        yield session

挑战 2:请求日志中间件

任务:创建一个中间件,记录每个请求的 URL、方法、IP 地址和处理时间。提示:使用 request.client.host 获取客户端 IP,使用 request.url 获取 URL。

今日产出物

  • 17_deps.py - 依赖函数定义
  • 17_main.py - 使用依赖注入和中间件的 FastAPI 应用

参考代码

查看参考代码

在 GitHub 查看完整的示例代码

在线运行

使用在线编辑器测试代码

实际应用场景

依赖注入在 API 开发中的应用

  • 鉴权验证:统一的 Token 验证逻辑,多个接口复用
  • 数据库连接:自动为每个请求提供数据库会话
  • 权限检查:根据用户角色检查是否有权限访问
  • 数据获取:从数据库或缓存中获取用户信息
  • 配置读取:统一读取配置信息

中间件在 API 开发中的应用

  • CORS 处理:处理跨域请求,前端开发必需
  • 请求日志:记录每个请求的详细信息
  • 性能监控:统计请求处理时间,定位性能瓶颈
  • 请求限流:限制请求频率,防止滥用
  • 错误处理:统一处理异常,返回友好的错误信息
与 Day 18 的关联:今天学习的依赖注入,明天会学习如何将数据库连接封装成依赖,实现优雅的数据库操作。

上一天: 数据验证

Day 16 | Pydantic 数据验证

下一天: 数据库整合

Day 18 | SQLAlchemy 数据库整合