1. 项目背景:从"百口锅"到"一口灶"的管理系统改造
去年秋季学期,我们小组接了个班级管理系统的重构任务。项目不算大,但前端调用后端接口的方式五花八门:有的接口返回 {"code":0,"data":...},有的直接丢一个 JSON 对象,有的把错误信息塞在 HTTP 状态码里,还有的干脆在业务逻辑里逐个写 try...except 然后拼错误消息。作为后端负责人,我发现最难受的事情不是写新功能,而是每次给接口加一个鉴权逻辑,就得在十几个路由函数里复制同一段 JWT 解析代码——这就是典型的"重复造轮子",而且每个轮子造得还不一样圆。
我当时的处境大概是这样的:系统里有教师端、学生端、管理员端三种角色,权限模型本身就有重叠,再加上课程管理、考勤统计、成绩录入这些业务模块,FastAPI 的 APIRouter 已经分了七八个文件。每次前端同学过来问我"为什么这个接口报错格式跟那个不一样",我都得翻半天代码。后来我痛下决心,用 FastAPI 中间件做了一次彻底统一:统一鉴权、统一日志、统一返回格式、统一异常处理。这篇文章把这套改造的完整思路、代码和踩过的坑都整理出来,给同样在做中小型管理系统后端的朋友一个参考。
当时团队里也有同学提出用装饰器或者依赖注入来做,甚至有人建议直接上微服务网关。我评估了一圈,最后选择中间件方案,核心原因有四点:一是中间件可以在不入侵业务代码的前提下拦下所有请求;二是班级管理系统这种体量,引入网关完全是杀鸡用牛刀;三是 FastAPI 中间件本身就是 ASGI 层面的标准机制,后续就算迁移到其他 ASGI 框架也能复用;四是中间件的执行顺序可控,可以叠加多个职责,正好匹配我们"统一管控"的需求。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心方案选型与整体架构设计
2.1 为什么是中间件,而不是装饰器或依赖注入
先回应一个很多人会问的问题:FastAPI 里做跨切面逻辑,装饰器、依赖注入、中间件都可以,为什么非要用中间件?
我简单做了一张对比表,是我当时给组内同学讲方案时用的:
| 实现方式 | 拦截范围 | 与业务代码耦合度 | 能否处理路由未匹配的请求 | 适合场景 |
|---|---|---|---|---|
| 装饰器 | 仅被装饰的接口 | 高,需要每个路由手动加 | 不能 | 单个接口的特有逻辑 |
| 依赖注入(Depends) | 声明了该依赖的路由 | 中,路由签名需修改 | 不能 | 参数校验、局部权限控制 |
| 中间件 | 所有经过应用的请求 | 低,业务代码无感知 | 能 | 全局统一逻辑 |
装饰器最大的问题是要记着给每个新接口加上。项目赶工期的时候,总有那么一两个接口漏加装饰器,然后权限漏洞就出来了。依赖注入稍微好一点,但它要求路由函数签名里显式写参数,这本身就污染了业务函数。中间件是请求进入路由之前的最后一层(实际是进入完整 ASGI 应用之前的一层),所有请求都从它这里过,不存在"漏加"的可能性。
再说一个关键细节:中间件能捕获到路由未匹配的 404 请求。班级管理系统里有几个前端路由是动态拼接的,比如 /api/student/{student_id}/courses,如果前端拼错了 ID 类型,FastAPI 会返回默认的 404 响应,这个响应格式和业务接口不一样。用了中间件之后,这种 404 也被统一包装成同样的返回结构,前端只要写一种解析逻辑就够了。
2.2 中间件在 FastAPI 请求生命周期中的位置
要设计好中间件,必须先理解它在请求生命周期里的位置。FastAPI 基于 Starlette,请求进来之后大致走这样一条链路:
text复制客户端请求
→ Uvicorn/Gunicorn
→ 最外层自定义中间件
→ 下一层自定义中间件
→ ...(按注册顺序逐层深入)
→ 路由匹配
→ 依赖注入
→ 路径/查询参数解析
→ 业务函数执行
→ 返回响应
→ 响应返回时逐层向外回传
→ 客户端收到响应
中间件就像洋葱皮,请求要一层层剥进去,响应要一层层返出来。这个特性很重要,因为它决定了你可以在请求进入业务逻辑之前做"预处理",也可以在响应返回之后做"后处理"。比如日志中间件,请求进来时记录一句话,响应出去时再记录一句话,两边一拼就是完整的调用信息。
我在项目里一共实现了四个中间件,按注册顺序从外到内是:
- 统一返回格式中间件(最外层)
- 异常捕获中间件
- JWT 认证与权限中间件
- 请求日志中间件(最内层)
这个顺序是我调了几次才定下来的。最外层放返回格式统一,可以保证所有响应的外壳都是一致的;认证放在比较靠里的位置,是因为它依赖请求体里的 token,而且它只关注 /api 下的业务路由,对 /docs 和静态资源要放行;日志放在最内层,可以记录到经过权限过滤后的最终请求状态。
2.3 四层中间件的职责边界
很多人在写中间件的时候容易犯一个错误:把什么逻辑都往中间件里塞。我的原则是每个中间件只干一件事,职责边界要清晰:
- 返回格式中间件:负责给所有响应包一层
{"code": 0, "data": ..., "message": "success"}的外壳; - 异常捕获中间件:把未捕获的异常转成统一格式的 JSON 响应,避免堆栈信息直接暴露给前端;
- 认证与权限中间件:从 Header 里解析 JWT,校验角色权限,不通过就直接返回 401/403;
- 日志中间件:打印请求方法、路径、处理耗时、状态码,方便排查问题。
这四个中间件互相不依赖,各自独立,组合起来却覆盖了之前散落在各个业务函数里的所有重复代码。等我把它们都实现完后,删除的业务代码量大概有 300 多行,同时新增接口时再也不用复制粘贴鉴权逻辑了。
3. 中间件核心实现与关键细节拆解
3.1 统一返回格式中间件:终结接口格式混乱
先上代码,这是整个改造中收益最明显的一个中间件:
python复制import json
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response, JSONResponse
class UnifiedResponseMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# 对非API路径直接放行,比如 /docs、/redoc、/static
if not request.url.path.startswith("/api"):
return await call_next(request)
# 执行下游(业务路由、其他中间
