FastAPI 中间件实战:统一请求处理的高效方案

0 阅读18分钟

在 FastAPI 应用开发中,随着接口数量的增多,很多接口会出现重复的处理逻辑——比如所有接口都需要验证用户身份、记录请求日志、监控接口性能,部分接口还需要处理跨域、设置统一响应头、过滤非法请求等。如果在每个接口的路由处理函数中重复编写这些逻辑,不仅会导致代码冗余、维护成本飙升,还可能出现逻辑不一致的问题,影响应用的稳定性。FastAPI 提供的中间件(Middleware)功能,恰好能解决这一痛点。中间件作为请求和响应的“全局拦截器”,会在每次请求进入应用时自动执行,在路由处理函数执行前后完成统一的逻辑处理,既能实现代码复用,又能实现全局管控。本文将聚焦中间件的核心用法,通过可直接运行的代码示例,详细讲解中间件的定义、核心作用、实际应用场景及多中间件执行顺序,补充更多细节说明,让内容更具实用性和完整性。

一、中间件核心概念

中间件是 FastAPI 中用于统一处理所有 HTTP 请求和响应的核心组件,本质上是一个被 @app.middleware("http") 装饰器修饰的异步函数(FastAPI 仅支持异步中间件)。它的执行时机具有固定性:第一次执行是在请求到达具体的路由处理函数之前(称为“请求预处理”),负责对请求进行拦截、校验、加工等操作;第二次执行是在路由处理函数执行完成、响应准备返回给客户端之前(称为“响应后处理”),负责对响应进行修改、补充、记录等操作。

简单来说,中间件就像一个“关卡”,所有请求都必须经过这个关卡才能到达接口,所有响应也必须经过这个关卡才能返回给客户端。这种机制能让我们将多个接口共用的逻辑集中管理,避免重复编码,让路由处理函数能够专注于核心业务逻辑,无需关注身份认证、日志记录等通用功能。中间件的核心作用主要包括:减少代码冗余、统一请求响应处理逻辑、实现全局管控(如全局认证、全局日志)、提升应用可维护性,常见的应用场景有身份认证、日志记录、性能监控、跨域处理、响应头统一设置等。

二、中间件的定义方式

FastAPI 中定义中间件的方式非常简洁,无需复杂的配置,只需遵循固定的语法规则即可:首先在异步函数顶部添加 @app.middleware("http") 装饰器,该装饰器指定中间件作用于 HTTP 请求;其次,函数必须接收两个必填参数——requestcall_next;最后,函数必须返回一个响应对象(Response),否则客户端无法接收响应。

这里对两个核心参数进行详细说明:request 是请求对象,包含了当前请求的所有信息,比如请求路径、请求方法、请求头、请求参数、请求体等,我们可以通过该对象获取请求相关数据,进行预处理操作;call_next 是一个异步回调函数,其作用是调用后续的中间件(如果有多个中间件)或当前请求对应的路由处理函数,执行完成后会返回一个响应对象,这是连接中间件和路由函数的关键。

基础示例:简单中间件定义

from fastapi import FastAPI

# 初始化FastAPI应用(debug模式便于开发调试,实际生产环境可关闭)
app = FastAPI(debug=True)

# 定义基础中间件:记录请求的基本信息
@app.middleware("http")
async def basic_middleware(request, call_next):
    # 1. 请求预处理:请求到达路由前执行,可获取请求信息并进行简单处理
    # 获取请求路径、请求方法、客户端IP
    request_path = request.url.path
    request_method = request.method
    client_ip = request.client.host  # 获取客户端IP地址
    print(f"【请求开始】- 客户端IP:{client_ip},请求路径:{request_path},请求方法:{request_method}")
    
    # 调用call_next(request),执行后续中间件(若有)或路由处理函数,获取响应对象
    # 这里必须使用await,因为call_next是异步函数
    response = await call_next(request)
    
    # 2. 响应后处理:响应返回客户端前执行,可获取响应信息并记录
    response_status = response.status_code  # 获取响应状态码
    print(f"【请求结束】- 响应状态码:{response_status},请求路径:{request_path}")
    
    # 必须返回响应对象,否则客户端会接收不到任何响应
    return response

# 测试接口1:GET请求,用于测试中间件的拦截效果
@app.get("/test")
async def test_interface():
    # 路由核心业务逻辑:返回简单的响应数据
    return {"msg": "测试接口响应", "code": 200, "data": None}

# 测试接口2:POST请求,验证中间件对不同请求方法的拦截
@app.post("/demo")
async def demo_interface():
    return {"msg": "演示接口响应", "code": 200, "data": {"name": "demo"}}

# 测试接口3:带路径参数的接口,验证中间件对带参数请求的拦截
@app.get("/user/{user_id}")
async def get_user(user_id: int):
    return {"msg": "获取用户信息成功", "code": 200, "data": {"user_id": user_id}}

启动应用后,访问任意接口(如 /test/demo/user/123),控制台都会依次输出请求预处理和响应后处理的日志。无论接口的请求方法是 GET 还是 POST,无论是否带有路径参数,中间件都会统一拦截处理,无需在每个接口中单独编写日志逻辑。这种方式极大地简化了通用逻辑的编写,后续若需修改日志格式,只需修改中间件代码即可,无需改动所有接口。

三、中间件的实际应用场景

中间件的应用场景非常广泛,几乎所有需要全局统一处理的逻辑,都可以通过中间件实现。以下结合实际开发中最常用的三个场景,提供独立构思的代码示例,每个示例都补充详细的注释和场景说明,确保实用性,同时避免重复,贴合真实开发需求。

场景1:身份认证中间件(全局鉴权)

在大多数后端应用中,除了登录、注册等公开接口外,其他接口都需要验证用户身份,确保只有合法用户才能访问。如果在每个需要认证的接口中都编写 Token 校验逻辑,不仅代码冗余,还容易出现遗漏,导致未认证用户访问敏感接口。通过中间件实现全局身份认证,可拦截所有请求,统一校验 Token,未通过认证则直接返回错误响应,无需在每个接口中重复编写校验逻辑。

from fastapi import FastAPI
from fastapi.responses import JSONResponse

app = FastAPI()

# 身份认证中间件:统一校验请求头中的Token,实现全局鉴权
@app.middleware("http")
async def auth_middleware(request, call_next):
    # 1. 排除公开接口,无需进行身份认证(如登录、注册接口)
    # 可将所有公开接口放入列表,方便后续维护
    public_paths = ["/login", "/register"]
    if request.url.path in public_paths:
        # 公开接口,直接执行后续逻辑,不进行认证校验
        response = await call_next(request)
        return response
    
    # 2. 非公开接口,从请求头中获取Token
    # 通常Token会放在请求头的Authorization字段中,格式一般为"Bearer Token值"
    auth_header = request.headers.get("Authorization")
    if not auth_header:
        # 未携带Authorization请求头,返回401身份认证失败
        return JSONResponse(
            status_code=401,
            content={"code": 401, "msg": "身份认证失败,请携带有效Token", "data": None}
        )
    
    # 3. 校验Token格式和有效性(实际开发中可结合JWT、Redis等方式实现更安全的校验)
    # 这里简化校验逻辑,实际开发中需替换为真实的Token校验逻辑
    token = auth_header.split(" ")[-1]  # 提取Token值(去除"Bearer "前缀)
    valid_token = "valid_token_123"  # 模拟有效Token(实际中从数据库或Redis获取)
    if token != valid_token:
        # Token无效,返回401错误
        return JSONResponse(
            status_code=401,
            content={"code": 401, "msg": "Token无效或已过期,请重新登录", "data": None}
        )
    
    # 4. Token认证通过,执行后续中间件或路由处理函数
    response = await call_next(request)
    return response

# 公开接口:登录接口(无需认证)
@app.post("/login")
async def login():
    # 模拟用户登录逻辑:验证用户名密码(此处省略),登录成功后返回Token
    return {
        "code": 200,
        "msg": "登录成功",
        "data": {"token": "valid_token_123", "username": "test_user"}
    }

# 公开接口:注册接口(无需认证)
@app.post("/register")
async def register():
    # 模拟用户注册逻辑(此处省略),注册成功后返回提示信息
    return {"code": 200, "msg": "注册成功", "data": None}

# 非公开接口:获取用户个人信息(需要认证)
@app.get("/user/info")
async def get_user_info():
    # 核心业务逻辑:查询用户信息并返回(此处省略数据库查询)
    return {
        "code": 200,
        "msg": "请求成功",
        "data": {"username": "test_user", "age": 25, "role": "normal"}
    }

# 非公开接口:获取用户列表(需要认证)
@app.get("/user/list")
async def get_user_list():
    # 核心业务逻辑:查询用户列表并返回(此处省略数据库查询)
    return {
        "code": 200,
        "msg": "请求成功",
        "data": [{"user_id": 1, "username": "test_user1"}, {"user_id": 2, "username": "test_user2"}]
    }

该示例中,中间件会对所有请求进行拦截,先判断请求路径是否为公开接口,若是则直接放行;若不是则校验请求头中的 Token,Token 未携带、格式错误或无效时,都会直接返回 401 身份认证失败响应,只有 Token 有效时才会执行后续的路由逻辑。这种方式实现了全局统一鉴权,后续新增需要认证的接口时,无需额外编写认证逻辑,只需确保接口路径不在公开接口列表中即可。

场景2:性能监控与日志记录中间件

在后端应用开发中,接口的性能监控和日志记录是必不可少的环节——需要记录每个接口的请求时间、执行耗时、请求参数、响应状态等信息,方便后续排查问题、优化接口性能。通过中间件统一实现这些功能,可避免在每个接口中重复编写日志和耗时统计逻辑,同时确保日志格式统一,便于后续日志分析和性能排查。

from fastapi import FastAPI
import time
import logging

app = FastAPI()

# 配置日志(实际开发中可配置日志输出到文件,便于后续查看)
logging.basicConfig(
    level=logging.INFO,  # 日志级别:INFO
    format="%(asctime)s - %(levelname)s - %(message)s",  # 日志格式
    datefmt="%Y-%m-%d %H:%M:%S"  # 时间格式
)
logger = logging.getLogger(__name__)

# 性能监控与日志记录中间件:统一记录请求详情和执行耗时
@app.middleware("http")
async def performance_log_middleware(request, call_next):
    # 1. 请求预处理:记录请求开始时间、请求详情
    start_time = time.time()  # 记录请求开始时间(时间戳)
    request_path = request.url.path  # 请求路径
    request_method = request.method  # 请求方法(GET/POST等)
    client_ip = request.client.host  # 客户端IP
    query_params = dict(request.query_params)  # 请求查询参数(如?page=1&size=10)
    
    # 记录请求日志(使用logging模块,而非print,便于后续日志管理)
    logger.info(
        f"请求开始 - 客户端IP:{client_ip},请求方法:{request_method},请求路径:{request_path},查询参数:{query_params}"
    )
    
    # 2. 执行后续逻辑,获取响应对象
    response = await call_next(request)
    
    # 3. 响应后处理:计算请求耗时,记录响应日志
    end_time = time.time()  # 记录请求结束时间
    cost_time = end_time - start_time  # 计算请求执行耗时(秒)
    response_status = response.status_code  # 响应状态码
    
    # 记录响应日志,耗时保留4位小数,便于精准监控性能
    logger.info(
        f"请求结束 - 请求路径:{request_path},响应状态码:{response_status},执行耗时:{cost_time:.4f}秒"
    )
    
    # 可根据耗时判断接口性能,耗时过长时记录警告日志
    if cost_time > 1.0:  # 若接口耗时超过1秒,记录警告日志
        logger.warning(f"接口性能警告 - 请求路径:{request_path},耗时过长:{cost_time:.4f}秒")
    
    return response

# 测试接口1:快速接口(模拟耗时较短的接口,如查询简单数据)
@app.get("/fast-interface")
async def fast_interface():
    # 模拟核心业务逻辑,耗时较短(小于0.1秒)
    return {
        "code": 200,
        "msg": "快速接口响应",
        "data": "该接口耗时较短,性能良好",
        "note": "适用于简单查询类接口"
    }

# 测试接口2:慢速接口(模拟耗时较长的接口,如复杂数据库查询、第三方接口调用)
@app.get("/slow-interface")
async def slow_interface():
    # 模拟耗时操作(如数据库查询、第三方接口调用),耗时0.8秒
    time.sleep(0.8)
    return {
        "code": 200,
        "msg": "慢速接口响应",
        "data": "该接口耗时较长,需优化",
        "note": "适用于复杂业务逻辑接口"
    }

# 测试接口3:带查询参数的接口
@app.get("/query-data")
async def query_data(page: int = 1, size: int = 10):
    # 模拟根据查询参数查询数据的逻辑
    return {
        "code": 200,
        "msg": "请求成功",
        "data": {"page": page, "size": size, "total": 100, "list": []}
    }

该示例中,中间件不仅记录了请求的基本信息(客户端IP、请求方法、路径、查询参数),还统计了接口的执行耗时,并通过 logging 模块记录日志(而非简单的 print),便于后续日志管理和分析。同时,添加了性能警告逻辑,当接口耗时超过1秒时,会记录警告日志,提醒开发者关注接口性能。启动应用后,访问任意接口,日志会按照配置的格式输出,既实现了统一的日志记录,又完成了接口性能监控,无需在每个接口中单独编写相关逻辑。

场景3:统一响应头与跨域处理中间件

在前后端分离项目中,跨域问题是常见的痛点,同时为了保证接口的安全性和规范性,通常需要为所有响应设置统一的响应头(如设置 Content-Type、X-Frame-Options 等)。通过中间件可统一设置所有响应的响应头,同时处理跨域请求,避免在每个接口中单独设置,确保响应头统一、跨域处理一致。

from fastapi import FastAPI
from fastapi.responses import JSONResponse

app = FastAPI()

# 统一响应头与跨域处理中间件
@app.middleware("http")
async def headers_cors_middleware(request, call_next):
    # 1. 请求预处理:处理跨域预检请求(OPTIONS请求)
    # 跨域预检请求是浏览器发送的试探性请求,用于判断服务器是否允许跨域
    if request.method == "OPTIONS":
        # 允许所有来源、所有请求方法、所有请求头
        response = JSONResponse(status_code=200)
        # 设置跨域相关响应头
        response.headers["Access-Control-Allow-Origin"] = "*"  # 允许所有来源(生产环境可指定具体域名)
        response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"  # 允许的请求方法
        response.headers["Access-Control-Allow-Headers"] = "Authorization, Content-Type"  # 允许的请求头
        return response
    
    # 2. 执行后续逻辑,获取响应对象
    response = await call_next(request)
    
    # 3. 响应后处理:设置统一的响应头
    # 设置响应内容类型为JSON
    response.headers["Content-Type"] = "application/json; charset=utf-8"
    # 设置X-Frame-Options,防止点击劫持攻击
    response.headers["X-Frame-Options"] = "DENY"
    # 设置跨域相关响应头(非预检请求也需要设置)
    response.headers["Access-Control-Allow-Origin"] = "*"
    # 设置服务器标识(可选,用于隐藏服务器信息,提升安全性)
    response.headers["Server"] = "FastAPI-Server"
    
    return response

# 测试接口:模拟前后端分离项目中的接口
@app.get("/api/data")
async def get_api_data():
    return {"code": 200, "msg": "请求成功", "data": {"key": "value"}}

@app.post("/api/submit")
async def submit_data():
    return {"code": 200, "msg": "提交成功", "data": None}

该示例中,中间件实现了两个核心功能:一是处理跨域预检请求(OPTIONS 请求),确保浏览器能够正常发起跨域请求;二是为所有响应设置统一的响应头,包括 Content-Type、X-Frame-Options、跨域相关头信息等。这样一来,所有接口的响应头都会保持统一,跨域请求也能正常处理,无需在每个接口中单独设置响应头或处理跨域逻辑,极大地简化了前后端分离项目的开发。

四、多个中间件的执行顺序

在实际开发中,往往需要同时使用多个中间件(如同时使用身份认证中间件、日志记录中间件、跨域处理中间件),这就需要明确多个中间件的执行顺序。FastAPI 中多个中间件的执行顺序遵循“自下而上”的核心规则,具体可分为两个阶段:

  1. 请求预处理阶段(call_next 之前的逻辑):按中间件定义的顺序“从下到上”执行,即后定义的中间件先执行预处理逻辑;

  2. 响应后处理阶段(call_next 之后的逻辑):按中间件定义的顺序“从上到下”执行,即先定义的中间件先执行后处理逻辑。

可以通俗地理解为:多个中间件就像一层一层的“包裹”,请求需要从最外层的中间件(后定义的)开始,逐层进入到最内层的路由处理函数;响应则需要从最内层的路由处理函数开始,逐层向外传递,经过所有中间件的后处理,最终返回给客户端。

from fastapi import FastAPI

app = FastAPI()

# 中间件A(定义在上方,先定义)
@app.middleware("http")
async def middleware_a(request, call_next):
    print("【中间件A】- 请求预处理:开始拦截请求")
    # 模拟请求预处理逻辑:比如记录请求日志
    response = await call_next(request)
    print("【中间件A】- 响应后处理:完成响应加工")
    # 模拟响应后处理逻辑:比如修改响应头
    return response

# 中间件B(定义在中间)
@app.middleware("http")
async def middleware_b(request, call_next):
    print("【中间件B】- 请求预处理:开始拦截请求")
    # 模拟请求预处理逻辑:比如身份认证
    response = await call_next(request)
    print("【中间件B】- 响应后处理:完成响应加工")
    # 模拟响应后处理逻辑:比如统计耗时
    return response

# 中间件C(定义在下方,后定义)
@app.middleware("http")
async def middleware_c(request, call_next):
    print("【中间件C】- 请求预处理:开始拦截请求")
    # 模拟请求预处理逻辑:比如跨域处理
    response = await call_next(request)
    print("【中间件C】- 响应后处理:完成响应加工")
    # 模拟响应后处理逻辑:比如设置统一响应头
    return response

# 测试接口:用于验证多中间件执行顺序
@app.get("/multi-middleware")
async def multi_middleware_test():
    print("【路由处理函数】- 执行核心业务逻辑")
    return {"msg": "多中间件测试成功", "code": 200}

运行应用后,访问 /multi-middleware,控制台输出顺序如下,清晰体现了多中间件的执行规则:

【中间件C】- 请求预处理:开始拦截请求
【中间件B】- 请求预处理:开始拦截请求
【中间件A】- 请求预处理:开始拦截请求
【路由处理函数】- 执行核心业务逻辑
【中间件A】- 响应后处理:完成响应加工
【中间件B】- 响应后处理:完成响应加工
【中间件C】- 响应后处理:完成响应加工

理解这一执行顺序,能帮助我们合理安排中间件的定义顺序,满足实际业务需求。例如,通常会将跨域处理中间件放在最外层(后定义),确保先处理跨域请求;将身份认证中间件放在中间,确保认证通过后再执行日志记录等逻辑;将日志记录中间件放在最内层(先定义),确保能记录最完整的请求和响应信息。

总结

FastAPI 中间件是实现请求和响应统一处理的高效工具,其核心价值在于减少代码冗余、实现全局管控、提升应用可维护性,是后端开发中不可或缺的组件。结合本文的详细讲解和代码示例,我们可以明确以下核心要点:

  • 核心概念:中间件是被@app.middleware("http") 修饰的异步函数,分为请求预处理和响应后处理两个阶段,相当于请求和响应的“全局拦截器”;
  • 定义方式:遵循固定语法,接收 request 和 call_next 两个必填参数,必须返回响应对象,参数和返回值缺一不可;
  • 实际应用:可用于身份认证、日志记录、性能监控、跨域处理、统一响应头等场景,每个场景都可通过中间件实现全局统一处理,避免重复编码;
  • 执行顺序:多个中间件时,请求预处理自下而上,响应后处理自上而下,需根据业务需求合理安排中间件定义顺序。

在实际开发中,合理使用中间件,能让我们将更多精力投入到核心业务逻辑的开发中,同时保证应用的规范性、稳定性和可维护性。无论是小型项目还是大型项目,中间件都能有效简化开发流程,提升开发效率,是 FastAPI 开发中的必备技巧。