路由(WebPath)
模块化与自省式路由系统 (WebPath)
为了构建一个可扩展、易于维护且具备自动化能力的大型 Web 应用,我们没有直接使用 Axum 的 Router 来零散地注册路由,而是设计了一套更高级的、基于 WebPath 结构体的 声明式路由构建系统。
这套系统的核心思想是:先将应用的全部路由结构描述为一个数据树 (Data Tree),然后再将这个树“编译”成一个真正的 Axum Router。
核心优势
- 模块化 (Modularity): 每个业务模块(如
sys_controll,wechat)可以独立定义自己的路由树,最后再统一合并。 - 自省 (Introspection): 在应用启动时,我们可以遍历整个路由树,获取所有 API 的信息(路径、方法、名称),并将其用于自动化任务,例如 自动更新接口权限数据库。
- 层级化 (Hierarchy): 使用
.nest()可以清晰地表达路由的层级关系,让代码结构与 URL 结构保持一致。 - 代码简洁 (Clean Code): 避免了在一个巨大的文件中注册所有路由,让代码库更加整洁。
核心概念:WebPath 结构体
WebPath 是我们路由系统的基本构建块。你可以把它想象成一个 文件系统中的目录或文件。
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct WebPath {
// 最终编译出的完整 URL 路径,如 "/sys/user/list"
pub final_path: String,
// HTTP 方法类型 (Get, Post, etc.)
pub webmethod: WebPathType,
// API 的可读名称,如 "获取用户列表",用于自动化
pub apiname: Option<String>,
// 存储实际的 Axum MethodRouter (Handler)
#[serde(skip)]
pub method_router: Option<MethodRouter>,
// 子路径,构成了路由树的“分支”
sub_paths: HashMap<String, WebPath>,
}- 一个
WebPath节点如果包含method_router,它就是一个 API 端点 (文件)。 - 如果它包含
sub_paths,它就是一个 路由分组 (目录)。
如何定义路由
我们通过链式调用的方式来构建 WebPath 树。
1. .route() - 定义一个 API 端点
使用 .route() 来注册一个具体的 API 接口。
use axum::routing::{get, post};
use crate::handler::sys_user_handler;
// 在一个专门的路由定义函数中
fn user_api_routes() -> WebPath {
WebPath::new()
// 定义获取列表接口
.route(
"/list", // 路径片段
WebPathType::Get, // HTTP 方法
Some("获取用户列表"), // API 名称 (用于自省)
get(sys_user_handler::list) // Axum Handler
)
// 定义新增用户接口
.route(
"/add",
WebPathType::Post,
Some("新增用户"),
post(sys_user_handler::add)
)
}2. .nest() - 嵌套路由(创建分组)
使用 .nest() 将一组相关的路由挂载到一个公共的路径前缀下。
// 将 user_api_routes() 挂载到 "/user" 路径下
fn user_module_router() -> WebPath {
WebPath::new().nest("/user", user_api_routes())
}
// 进一步将 user_module_router() 挂载到 "/sys" 路径下
pub fn router_sys() -> WebPath {
WebPath::new().nest("/sys", user_module_router())
}经过这样的嵌套,我们最终得到的路径将是 /sys/user/list 和 /sys/user/add。
3. .merge() - 合并不同模块的路由
使用 .merge() 可以将多个独立的 WebPath 树合并到同一个层级。这在组合顶级模块时非常有用。
// 在 WebApi::routers() 中
let mut webpath = WebPath::new();
// 合并系统模块的路由树
webpath = webpath.merge(sys_controll::router_sys());
// 合并微信模块的路由树
webpath = webpath.merge(wechat::router_wechat());路由的编译与自省过程
WebApi::routers() 函数是整个系统的“主引擎”,它负责将我们用 WebPath 描述的路由树转换为 Axum 可以使用的 Router,并在此过程中执行自省操作。
这个过程分为以下几个关键步骤:
// WebApi::routers()
pub fn routers() -> Router {
// 1. 创建一个空的根节点
let mut webpath = WebPath::new();
// 2. [组合] 合并所有顶级模块的路由定义
webpath = webpath.merge(sys_controll::router_sys());
webpath = webpath.merge(wechat::router_wechat());
// 3. [编译] 递归遍历路由树,计算每个端点的最终完整路径
// 例如,将 ("/sys", ("/user", "/list")) 编译成 final_path: "/sys/user/list"
webpath = webpath.final_to_path();
// 4. [自省] 获取所有最末端的 API 端点(叶子节点)
let expand_path = webpath.get_last_level_paths();
// 5. [自动化] 将所有 API 信息序列化为 JSON,并发送给一个后台 Worker
// 这个 Worker (InvokeFunctionWorker) 会调用一个名为 "updateapi" 的函数,
// 其作用可能是将最新的 API 列表更新到数据库的权限表中。
let invfun = InvokeFunctionMsg {
callfun: "updateapi".to_owned(),
parmets: serde_json::to_string(&expand_path).unwrap(),
};
tokio::spawn(async move {
let _ = InvokeFunctionWorker::execute_async(invfun).await;
});
// 6. [构建] 遍历扁平化后的 API 列表,构建最终的 Axum Router
let mut router = Router::new();
for p in expand_path {
if let Some(method_router) = p.method_router.clone() {
router = router.route(&p.final_path, method_router);
}
}
Router::new().merge(router)
}自动化任务:updateapi
什么是“自省式自动化”?
步骤 5 是这套系统最强大的特性。由于我们拥有一个包含所有 API 元数据(路径、方法、名称)的完整数据结构,我们可以在应用启动时:
- 自动注册权限:将所有 API 自动写入权限控制表,新接口无需手动在数据库中添加。
- 生成 API 文档:自动生成 Swagger/OpenAPI 文档。
- API 监控:将所有 API 列表注册到监控系统。
这是简单的 Router::new().route(...) 链式调用无法实现的。
核心设计:受保护路由与公共路由的分离
在我们的应用中,并不是所有的 API 都需要用户登录后才能访问。例如,登录、注册、获取验证码等接口必须是公开的。为了安全、清晰地管理不同权限级别的路由,我们将路由注册分成了两个独立的部分:
WebApi::routers(): 用于注册 受保护的 API(需要 Token 验证)。WebApi::white_routers(): 用于注册 公共白名单 API(无需 Token 验证)。
这种分离设计使得我们可以在应用的主入口处(main.rs)为它们应用不同的中间件(Middleware)。
1. WebApi::routers() - 受保护的路由
这是我们应用中绝大多数业务 API 的注册入口。
- 职责: 构建需要用户登录和权限验证的 API 路由。
- 特性:
- 启用自省: 它会调用
.final_to_path()和.get_last_level_paths()来收集所有 API 的元数据。 - 触发自动化任务: 它会将收集到的 API 列表发送给
InvokeFunctionWorker,用于自动更新数据库中的权限表。
- 启用自省: 它会调用
- 适用场景: 所有涉及用户数据操作、需要身份认证的业务接口,例如:
- 获取用户个人资料 (
/sys/user/profile) - 创建订单 (
/order/create) - 查询后台数据报表 (
/dashboard/stats)
- 获取用户个人资料 (
在 main.rs 中的应用示例:
// main.rs
// ... 其他代码 ...
let protected_api = WebApi::routers()
.layer(
// 在这里应用 JWT 认证中间件
middleware::from_fn_with_state(app_state.clone(), jwt::auth)
);
let app = Router::new()
.merge(protected_api) // 合并受保护的路由
// ... 合并其他路由 ...通过 .layer(),我们为 WebApi::routers() 生成的所有路由统一添加了 jwt::auth 中间件,实现了访问控制。
2. WebApi::white_routers() - 公共白名单路由
这个入口专门用于注册那些 任何人都应该能够访问 的公共接口。
- 职责: 构建无需任何身份验证的 API 路由。
- 特性:
- 简洁直接: 它不执行复杂的自省或自动化任务,仅仅是简单地合并各个模块提供的白名单路由。
- 高性能: 由于不需要进行自省计算,它的构建速度非常快。
- 适用场景:
- 用户登录 (
/sys/login) - 用户注册 (
/sys/register) - 获取公开信息,如新闻列表 (
/public/news) - 微信公众号、支付回调等外部服务接口 (
/wechat/notify)
- 用户登录 (
在 main.rs 中的应用示例:
// main.rs
// ... 其他代码 ...
let public_api = WebApi::white_routers(); // 无需认证中间件
let protected_api = WebApi::routers()
.layer(middleware::from_fn_with_state(app_state.clone(), jwt::auth));
let app = Router::new()
.merge(public_api) // 先合并公共路由
.merge(protected_api) // 再合并受保护的路由
// ...由于 public_api 没有应用认证中间件,它下面的所有路由都是公开可访问的。
开发者规范与最佳实践
安全警告:请务必遵守!
默认受保护原则: 在为新功能添加 API 时,默认应该将其放入受保护的路由 (
router_sys(), etc.)。只有当你能明确回答“这个接口是否允许未登录的匿名用户访问?”并且答案是“是”时,才应将其放入白名单路由函数(如white_sys())。职责分离:
sys_controll::router_sys(): 定义系统管理模块中 需要认证 的所有路由。sys_controll::white_sys(): 定义系统管理模块中 无需认证 的路由(主要是登录/注册)。- 其他模块(如
wechat)也应遵循此模式,提供router_...()和router_..._white()两个函数。
最小权限原则: 白名单中的接口应尽可能少。任何涉及敏感信息或会修改数据的操作都 严禁 放入白名单。
通过严格遵守这种路由分离的规范,我们可以从架构层面保证应用的安全性,避免因开发者疏忽而导致的安全漏洞。
最佳实践与开发规范
- 模块化:每个业务模块(如
user,role,dept)都应该有自己的路由定义文件,并提供一个导出WebPath的主函数。 - 层级化:善用
.nest()来组织路由,保持代码结构和 URL 结构的一致性。 - 命名:为每个
.route()提供一个清晰、有意义的apiname,这将直接影响到自动化任务(如权限名称)的质量。 - 白名单:对于不需要鉴权的接口(如登录、注册),应在专门的 "white" 路由函数中定义(如
sys_controll::white_sys()),以便于在中间件中统一处理。