前阵子我重构班级管理系统的后端,把散落在几十个接口里的登录校验、权限判断、异常捕获、日志记录全删了,改成一层层中间件统一管控。删完之后工程清爽得不像话,新增接口的时候只需要写业务代码,其他横切逻辑一概不用管。今天就把这次从“重复造轮子”到“统一管控”的完整过程拆开来聊聊,内容包括中间件原理、实战代码、踩坑记录和架构取舍,适合正在用 FastAPI 写后端、又苦于重复代码太多的同学参考。
1. 项目背景:为什么我会走上“重复造轮子”这条路
1.1 最初的代码写成了什么样
班级管理系统早期功能简单,后端就几个模块:学生管理、教师管理、课程安排、成绩录入、公告发布。刚开始接口少,我习惯在每个路由函数里直接写校验逻辑。就拿“获取学生列表”这个接口来说,几十行代码里有大半跟业务无关:
python复制@router.get("/students")
async def list_students(request: Request):
token = request.headers.get("Authorization")
if not token:
return {"code": 401, "msg": "未登录"}
user = verify_token(token)
if not user:
return {"code": 401, "msg": "登录已过期"}
if user.role != "admin":
return {"code": 403, "msg": "无权限访问"}
try:
students = await get_students_from_db()
return {"code": 0, "msg": "ok", "data": students}
except Exception as e:
logger.error(f"查询学生列表失败: {e}")
return {"code": 500, "msg": "服务器内部错误"}
这个写法在接口少的时候没什么问题。但班级管理系统功能越加越多,考勤、选课、成绩分析、家长通知,接口数量很快突破三十个。矛盾就来了:上面的 token 校验、角色判断、异常捕获、统一返回结构,我几乎在每个接口里都复制了一遍。最夸张的时候,改一个 token 过期判断逻辑,要全局搜索替换七八处。
1.2 重复代码带来的三个痛点
第一是维护成本爆炸。某个接口漏改,认证逻辑就跟其他接口不一致,前端一会儿收到 {"code": 401},一会儿收到 {"detail": "Not authenticated"},联调的时候被问得怀疑人生。
第二是安全漏洞风险。班级系统里有学生、教师、管理员三种角色,权限边界本来就细。手写校验最大的隐患是容易漏写。有一次“导出成绩单”的接口忘了加管理员判断,学生登录后直接传 student_id 就能拿到全班成绩,这种问题在代码评审里非常难发现,因为每个接口的校验代码长得差不多,review 的人很容易视觉疲劳。
第三是接口表现不一致。同样一个“无权限”错误,有的接口返回 HTTP 403,有的返回 HTTP 200 + code=403,前端 axios 拦截器很难统一处理。根治这个问题,必须把横切逻辑从业务代码里剥离出去。
1.3 为什么最终选了中间件而不是装饰器或依赖注入
很多同学第一反应是用装饰器。装饰器确实能解决“重复”的问题,但它有个明显的短板:只能作用在单个路由函数上,没办法在“请求进入路由之前”和“响应返回客户端之前”这两个更早或更晚的时机做统一处理。比如 CORS 跨域、访问日志、请求耗时统计、全局异常兜底,这些逻辑用装饰器会很别扭,而且路由函数一旦从装饰器里抛异常,处理起来非常被动。
FastAPI 的依赖注入(Depends)更适合处理“参数级”的复用,比如解析当前用户、校验分页参数。但它同样依赖你在每个接口里显式声明参数,做不到真正的“零侵入”。
中间件则完全不同。它挂在 ASGI 应用的最外层,顺序经过所有路由。请求进来时先过中间件,再进路由;响应返回时先经过路由,再经过中间件。登录校验、权限控制、请求日志、耗时统计、全局异常捕获,这些横切关注点放中间件里干,业务代码就变得非常干净。这也是我这次优化的核心思路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. FastAPI中间件底层原理:搞懂执行顺序才能定方案
2.1 中间件在请求生命周期中的位置
要理解中间件,先看 FastAPI 处理一个请求的完整链路。客户端请求到达 Uvicorn 后,会依次经过服务端错误中间件、你注册的各个中间件、异常处理中间件,最后才进入路由匹配和端点函数。响应返回时顺序反过来。
这个顺序直接影响方案设计。比如日志中间件要记录完整耗时,就必须放在最外层;认证中间件要拦截未登录请求,就要放在路由之前;CORS 中间件要处理预检请求,就得放在认证外层,否则浏览器发 OPTIONS 预检请求会先被认证中间件拦截,跨域直接失败。
写代码之前先把这个链路搞清楚,后面能少踩很多坑。
2.2 BaseHTTPMiddleware 与纯 ASGI 中间件
FastAPI 官方文档推荐基于 starlette.middleware.base.BaseHTTPMiddleware 写中间件,重写 dispatch 方法就可以。我之前就是这么干的,写法简单,上手快:
python复制from starlette.middleware.base import BaseHTTPMiddleware
class MyMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
# 请求前置处理
response = await call_next(request)
# 响应后置处理
return response
不过后来压测发现,BaseHTTPMiddleware 在流式响应(比如下载 CSV、文件导出)场景下会有额外性能开销,因为 call_next 返回的 response 会被包装一层,流式内容可能被缓冲。如果不涉及大文件流式下载,这个差异可以忽略。但如果你追求极致性能,可以直接写纯 ASGI 中间件:
python复制class PureASGIMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
# 请求前置处理
await self.app(scope, re
