> ## Documentation Index
> Fetch the complete documentation index at: https://docs.akria.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Day 17 | 依赖注入与中间件

> 解密 FastAPI 的核心魔法：Depends 依赖注入系统与 Request 中间件

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

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="book" size={26} color="#059669" /> 学习内容 (30 mins)</span>

在开始写代码前，先搞懂这些核心概念，否则后面的代码你会看得云里雾里。

<AccordionGroup>
  <Accordion title="依赖注入 (Dependency Injection) (15 mins)" icon="puzzle">
    **什么是依赖注入？**

    依赖注入是一种设计模式，将依赖关系从代码内部移到外部，通过参数传递的方式提供依赖。

    **为什么需要依赖注入？**

    * **代码复用**：将通用逻辑（如鉴权、数据库连接）封装成函数，多个接口复用
    * **解耦代码**：业务逻辑和横切关注点（cross-cutting concerns）分离
    * **易于测试**：可以轻松替换依赖，便于单元测试
    * **自动执行**：FastAPI 自动执行依赖函数，无需手动调用

    **传统方式 vs 依赖注入**：

    **传统方式**：

    ```python theme={null}
    # 每个接口都要写一遍鉴权逻辑
    @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
        # 业务逻辑...
    ```

    **依赖注入方式**：

    ```python theme={null}
    # 定义一次，到处复用
    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` 成功，将返回值传递给路由函数（如果需要）
  </Accordion>

  <Accordion title="中间件 (Middleware) (15 mins)" icon="layer-group">
    **什么是中间件？**

    中间件是在请求处理前后执行的函数，可以拦截所有请求和响应。

    **为什么需要中间件？**

    * **全局处理**：对所有请求执行相同的操作（如日志记录、CORS 处理）
    * **请求拦截**：在请求到达路由前进行处理
    * **响应修改**：在响应返回前添加头部、修改内容

    **中间件 vs 依赖注入**：

    * **依赖注入 (Depends)**：
      * 作用于**特定**的路由函数
      * 适合业务强相关的逻辑（如：用户必须登录）
      * 可以传递返回值给路由函数
    * **中间件 (Middleware)**：
      * 作用于**全局**所有请求
      * 适合通用的横切关注点（如：记录请求耗时、CORS 处理）
      * 不能直接传递数据给路由函数

    **类比**：

    * **中间件**：小区保安（谁进小区都得看一眼）
    * **依赖注入**：门禁卡（只有进自家单元门才刷卡）

    **中间件的执行顺序**：

    1. 请求进入 → 中间件 1 → 中间件 2 → ... → 路由函数
    2. 路由函数执行
    3. 响应返回 → 中间件 2 → 中间件 1 → 客户端

    **常见中间件应用**：

    * **CORS**：处理跨域请求
    * **日志记录**：记录每个请求的 URL、方法、耗时
    * **请求验证**：验证请求格式、大小限制
    * **响应压缩**：压缩响应内容
  </Accordion>
</AccordionGroup>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="code" size={26} color="#059669" /> 代码任务 (90 mins)</span>

<Steps>
  <Step title="环境准备">
    确保虚拟环境已激活：

    ```bash theme={null}
    # 确保虚拟环境已激活
    source .venv/bin/activate

    # FastAPI 和 Uvicorn 应该已经安装（Day 15）
    # 如果没有，执行：pip install fastapi uvicorn
    ```
  </Step>

  <Step title="任务 A：编写通用鉴权依赖">
    创建一个可复用的鉴权依赖函数。

    **任务分解**：

    1. 创建依赖函数，从 Header 中读取 Token
    2. 验证 Token 是否有效
    3. 如果无效，抛出异常

    <CodeGroup>
      ```python 17_deps.py theme={null}
      #!/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"
          }
      ```
    </CodeGroup>

    **代码解释**：

    * **`Header(...)`**：从 HTTP Header 中获取参数
    * **`HTTPException`**：抛出 HTTP 异常，FastAPI 自动转换为错误响应
    * **依赖链**：`get_current_user` 可以依赖 `verify_token`，形成依赖链

    **验证步骤**：

    1. 检查文件语法是否正确
    2. 尝试导入：`python -c "from 17_deps import verify_token; print('OK')"`
  </Step>

  <Step title="任务 B：编写耗时统计中间件">
    创建一个中间件，统计每个请求的处理时间。

    <CodeGroup>
      ```python 17_main.py theme={null}
      #!/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"}
      ```
    </CodeGroup>

    **代码解释**：

    * **`CORSMiddleware`**：处理跨域请求，前端开发必需
    * **`@app.middleware("http")`**：定义 HTTP 中间件
    * **`call_next(request)`**：调用下一个处理环节（路由函数）
    * **`dependencies=[Depends(...)]`**：在路由级别使用依赖
    * **`current_user: dict = Depends(...)`**：接收依赖函数的返回值

    **运行脚本**：

    ```bash theme={null}
    # 启动服务器
    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`
       * 应该能看到用户信息

    **常见错误**：

    <Warning>
      * ❌ `422 Validation Error` - Header 参数缺失或格式错误，检查 Header 名称和值
      * ❌ `400 Invalid X-Token header` - Token 验证失败，检查 Token 值是否正确
      * ❌ `CORS error` - 跨域请求被阻止，检查 CORS 中间件配置
    </Warning>
  </Step>
</Steps>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="lightbulb" size={26} color="#059669" /> 拓展任务 (30 mins)</span>

<CardGroup cols={2}>
  <Card title="挑战 1：数据库连接依赖" icon="database">
    **任务**：创建一个数据库连接依赖，自动为每个请求提供数据库会话。

    **提示**：

    ```python theme={null}
    async def get_db():
        async with AsyncSessionLocal() as session:
            yield session
    ```
  </Card>

  <Card title="挑战 2：请求日志中间件" icon="file">
    **任务**：创建一个中间件，记录每个请求的 URL、方法、IP 地址和处理时间。

    **提示**：使用 `request.client.host` 获取客户端 IP，使用 `request.url` 获取 URL。
  </Card>
</CardGroup>

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="file" size={26} color="#059669" /> 今日产出物</span>

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

***

## <span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}><Icon icon="github" size={26} color="#059669" /> 参考代码</span>

<CardGroup cols={2}>
  <Card title="查看参考代码" icon="code" href="https://github.com/akriamail/insightful-ops/tree/main/docs/mintlify/scripts/training/30daystudy/week-3/day-17">
    在 GitHub 查看完整的示例代码

    <br />

    <small>包含依赖注入和中间件示例</small>
  </Card>

  <Card title="在线运行" icon="play" href="https://replit.com">
    使用在线编辑器测试代码

    <br />

    <small>无需本地环境配置</small>
  </Card>
</CardGroup>

***

## 实际应用场景

<CardGroup cols={2}>
  <Card title="依赖注入在 API 开发中的应用" icon="server">
    * **鉴权验证**：统一的 Token 验证逻辑，多个接口复用
    * **数据库连接**：自动为每个请求提供数据库会话
    * **权限检查**：根据用户角色检查是否有权限访问
    * **数据获取**：从数据库或缓存中获取用户信息
    * **配置读取**：统一读取配置信息
  </Card>

  <Card title="中间件在 API 开发中的应用" icon="code">
    * **CORS 处理**：处理跨域请求，前端开发必需
    * **请求日志**：记录每个请求的详细信息
    * **性能监控**：统计请求处理时间，定位性能瓶颈
    * **请求限流**：限制请求频率，防止滥用
    * **错误处理**：统一处理异常，返回友好的错误信息
  </Card>
</CardGroup>

<Tip>
  **与 Day 18 的关联**：今天学习的依赖注入，明天会学习如何将数据库连接封装成依赖，实现优雅的数据库操作。
</Tip>

***

<CardGroup cols={2}>
  <Card title="上一天: 数据验证" href="/training/30daystudy/day-16">
    Day 16 | Pydantic 数据验证
  </Card>

  <Card title="下一天: 数据库整合" href="/training/30daystudy/day-18">
    Day 18 | SQLAlchemy 数据库整合
  </Card>
</CardGroup>
