中间件
2024/10/30大约 6 分钟
中间件架构
中间件是我们应用中处理 HTTP 请求的核心机制。它允许我们在请求到达最终的业务处理函数 (Handler) 之前或之后,执行一系列通用的、可复用的操作,例如日志记录、身份验证、权限检查等。
我们的中间件系统遵循经典的 “洋葱模型 (Onion Model)”。你可以将每个中间件想象成一层洋葱皮,请求从外层穿透到内层,最终到达核心(Handler),然后响应再从内层穿透回外层。
中间件执行流程(洋葱模型)
一个典型的受保护请求会按照以下顺序流经我们的中间件:
(这是一个形象化的流程图)
- Request In ->
OperateLogMid(操作日志中间件 - 开始计时)- ->
AuthMid(请求上下文构建中间件) - ->
from_extractor::<UserInfo>()(JWT 身份认证中间件) - ->
ApiMid(API 权限验证中间件) - ->
Your Handler(核心业务逻辑) - <-
ApiMid - <-
from_extractor::<UserInfo>() - <-
AuthMid - <-
OperateLogMid(操作日志中间件 - 计算耗时并记录日志) - <- Response Out
核心组件:请求扩展 (Request Extensions)
中间件之间并不是孤立的。它们通过 Axum 的 请求扩展 (Request Extensions) 机制来共享数据。前一个中间件处理完的数据(如用户信息)可以被“附加”到请求上,供后续的中间件或 Handler 使用。
我们主要使用两个自定义的扩展结构体:
ReqCtx: 包含了当前请求的上下文信息,如规范化的路径、方法、参数等。UserInfo: 包含了从 JWT Token 中解析出的当前登录用户的信息。
中间件详解
下面我们按照请求的执行顺序,详细解析每一层中间件的职责。
1. OperateLogMid - 操作日志中间件
这是 最外层 的中间件,它包裹了整个请求生命周期。
- 职责: 记录详细的操作日志,包括请求信息、响应数据、执行耗时、用户信息等。
- 核心流程:
- 在请求进入时,从请求扩展中获取
ReqCtx和UserInfo(如果存在)。 - 启动一个计时器。
- 调用
next.run(req).await将请求传递给下一层中间件。 - 等待 内层所有处理完成后,获取到最终的
Response。 - 停止计时器,计算出总耗时。
- 从响应扩展中提取响应体数据 (
RespDataString)。 - 异步 (
tokio::spawn) 地将所有收集到的信息(请求上下文、用户信息、耗时、响应数据等)写入到日志系统。
- 在请求进入时,从请求扩展中获取
- 关键设计:
- 异步非阻塞日志: 日志写入操作在一个独立的 Tokio 任务中执行,不会阻塞 最终响应返回给客户端,保证了 API 的低延迟。
- 可配置日志目标:
oper_log_add_fn函数会根据 API 在缓存中的配置 (apipermiss.logcache),智能地决定是将日志写入 文件 (file_log)、数据库 (db_log),还是两者都写。
2. AuthMid - 请求上下文构建中间件 (auth_fn_mid)
这一层是数据准备的关键环节。
- 职责: 解析原始的 HTTP 请求,并构建一个结构化的
ReqCtx对象,方便后续中间件和日志系统使用。 - 核心流程:
- 从请求中提取 URI、Path、Query、Method 等原始信息。
- 读取并消耗 (consume) 整个请求体 (Request Body)。
- 将解析出的信息组装成一个
ReqCtx实例。 - 将
ReqCtx实例通过req.extensions_mut().insert()注入到请求扩展中。 - 重新构建 (rebuild) 一个包含原始请求体的新
Request对象,并将其传递给下一层。
- 关键设计:
- 请求体重建: Axum 的
Body只能被读取一次。此中间件通过先完整读取 Body,然后用读取到的字节重新创建一个 Body 的方式,解决了后续 Handler 无法再次读取 Body 的问题。这是处理请求体的标准模式。
- 请求体重建: Axum 的
3. from_extractor::<UserInfo>() - JWT 身份认证中间件
这是我们安全体系的 第一道防线:身份认证 (Authentication)。
- 职责: 验证请求中是否包含有效、未过期的 JWT Token,并解析出用户信息。
- 核心流程:
- 这是一个由 Axum 提供的特殊中间件,它本质上是执行了
UserInfo提取器 (Extractor) 的逻辑。 - 它会自动从请求头的
Authorization: Bearer <token>中提取 Token。 - 验证 Token 的签名、有效期等。
- 如果验证成功,它会将解析出的
UserInfo结构体 注入到请求扩展中。 - 如果 Token 不存在、无效或已过期,它会 立即中断请求,并直接返回一个
401 Unauthorized错误响应。后续的中间件和 Handler 将不会被执行。
- 这是一个由 Axum 提供的特殊中间件,它本质上是执行了
4. ApiMid - API 权限验证中间件 (api_fn_mid)
这是我们安全体系的 第二道防线:授权 (Authorization)。
- 职责: 检查当前登录的用户是否有权限访问其正在请求的 API。
- 核心流程:
- 从请求扩展中获取由前两层中间件注入的
ReqCtx和UserInfo。 - 使用用户的角色 ID (
user.rid) 和请求的路径 (ctx.path)、方法 (ctx.method),调用s_sys_role_api::check_api_permission服务进行权限检查。 check_api_permission服务会查询数据库或缓存,判断该用户的角色是否被授予访问此 API 的权限。- 如果 有权限 (
apiauth为true),则调用next.run(req).await将请求放行到最终的 Handler。 - 如果 没有权限 (
apiauth为false),则 立即中断请求,并返回一个403 Forbidden或404 Not Found的错误响应(此处实现为404,可以隐藏 API 存在的事实)。
- 从请求扩展中获取由前两层中间件注入的
如何应用中间件
在 main.rs 中,我们将这个中间件链通过 .layer() 应用到所有受保护的路由上。
// main.rs
use axum::middleware;
// ...
// 1. 获取受保护的路由
let protected_api = WebApi::routers();
// 2. 将中间件链(洋葱)应用到路由上
let protected_api_with_middleware = protected_api
// 最外层: 操作日志
.layer(middleware::from_fn(operate_log_fn_mid))
// 第二层: 上下文构建
.layer(middleware::from_fn(auth_fn_mid))
// 第三层: JWT 认证
.layer(middleware::from_extractor_with_state::<UserInfo, AppState>(app_state.clone()))
// 最内层: API 权限验证
.layer(middleware::from_fn(api_fn_mid));
// 3. 将应用了中间件的路由合并到主应用中
let app = Router::new()
.merge(public_api)
.merge(protected_api_with_middleware);
// ...注意: .layer() 的调用顺序非常重要,它决定了“洋葱”的层次。先调用的 .layer() 在更外层。 (注:根据您提供的代码,实际的中间件函数名可能与 set_auth_middleware 中的别名不同,本文档使用了实际的函数名进行解释。)