授权(RBAC/数据权限)
大约 5 分钟
RBAC 权限系统详解
在我们的应用中,仅仅验证用户是谁(身份认证 Authentication)是不够的,我们还需要确定该用户 被允许做什么(授权 Authorization)。这套授权机制是通过一个经典的 基于角色的访问控制(RBAC - Role-Based Access Control) 系统来实现的。
本文档将详细解析 RBAC 系统的工作流程,从构建请求上下文,到最终的权限校验。
核心理念:用户 -> 角色 -> 权限
RBAC 系统的基本思想非常直观:
- 权限 (Permission):权限是系统中最小的授权单位,它通常被定义为“对某个 API(如
/sys/user/add
)的特定操作(如POST
)的许可”。 - 角色 (Role):角色是 一组权限的集合。例如,“管理员”角色可能包含对所有用户管理 API 的增删改查权限,而“访客”角色可能只包含对文章列表 API 的读取权限。
- 用户 (User):每个用户被分配 一个或多个角色。用户最终拥有的总权限,是他所持有的所有角色的权限的并集。
通过这种方式,我们无需为每个用户单独配置权限,只需将用户加入适当的角色,即可实现权限的批量管理。
工作流程:从请求到授权决策
当一个经过 JWT 身份认证的请求到达我们的受保护路由时,它会依次流经以下两个关键的中间件来完成授权检查:
第 1 步:AuthMid
- 构建请求上下文 (ReqCtx
)
在进行权限检查之前,我们需要准确地知道“用户正在请求什么”。AuthMid
(auth_fn_mid
) 中间件就是为此而生。
- 职责:
- 解析请求: 从原始的 HTTP 请求中提取出关键信息:方法 (
method
)、路径 (path
)、查询参数 (path_params
) 等。 - 规范化路径: 将路径进行处理,例如移除全局的
/api
前缀,得到一个用于权限匹配的、干净的 API 路径。 - 构建
ReqCtx
: 将所有解析出的信息封装到一个结构化的ReqCtx
对象中。 - 注入扩展: 将
ReqCtx
实例通过req.extensions_mut().insert()
注入到请求扩展中,供后续的中间件和 Handler 使用。 - 重构请求体: 为了不影响后续 Handler 读取请求体,此中间件会先完整读取 Body,然后用读取到的字节重新创建一个 Body。
- 解析请求: 从原始的 HTTP 请求中提取出关键信息:方法 (
第 2 步:ApiMid
- 权限验证 (api_fn_mid
)
这是 RBAC 系统的核心决策点。在 AuthMid
和 UserInfo
提取器都成功执行后,ApiMid
开始工作。
- 职责:
- 获取上下文: 从请求扩展中获取由前两层准备好的
ReqCtx
(请求什么) 和UserInfo
(谁在请求)。 - 调用权限检查服务: 将用户的角色 ID (
user.rid
)、请求的 API 路径 (ctx.path
) 和方法 (ctx.method
) 作为参数,调用核心的权限检查函数s_sys_role_api::check_api_permission
。 - 决策:
- 如果
check_api_permission
返回true
(有权限),则调用next.run(req).await
将请求放行到最终的 Handler。 - 如果
check_api_permission
返回false
(无权限),则 立即中断请求,并返回一个403 Forbidden
或404 Not Found
的错误响应,阻止未经授权的访问。
- 获取上下文: 从请求扩展中获取由前两层准备好的
权限检查的深层逻辑 (check_api_permission
)
check_api_permission
函数内部封装了我们权限系统的核心规则:
pub async fn check_api_permission(rid: i64, api: &str, method: &str) -> bool {
// 规则 1: 超级管理员豁免
// 检查用户的角色 ID 是否在配置文件的“超级角色”列表中。
// 如果是,则直接返回 true,跳过所有后续的数据库查询。
if APPCOFIG.system.super_role.contains(&rid) {
return true;
}
// 规则 2: 标准角色-API 权限查询
// 对于普通角色,构造查询参数...
let arg = RoleApiCheckInfo {
role_id: rid,
api: api.to_owned(),
method: method.to_owned(),
};
// ...并调用 model 层的方法去数据库中精确匹配。
SysRoleApiModel::check_api(arg).await
}
SysRoleApiModel::check_api
这个函数执行最终的数据库查询:
pub async fn check_api(arg: RoleApiCheckInfo) -> bool {
// 在 `sys_role_api` 关联表中查找是否存在一条记录,
// 其 role_id, api, method 同时与传入的参数匹配。
let model = sys_role_api::Entity::find()
.filter(
Condition::all()
.add(sys_role_api::Column::RoleId.eq(arg.role_id))
.add(sys_role_api::Column::Api.eq(arg.api))
.add(sys_role_api::Column::Method.eq(arg.method)),
)
.one(DB().await)
.await
.unwrap();
// 如果找到了记录 (model.is_some() 为 true),则代表有权限。
// 否则,没有权限。
model.is_some()
}
如何为 API 配置权限
作为开发者,你的职责是通过我们的 模块化路由系统 正确地注册 API 并提供 apiname
。API 的权限分配工作则由 系统管理员在后台管理界面完成。
一个典型的权限配置流程:
- API 自动注册: 当应用启动时,路由系统的自省功能会自动将所有受保护的 API 路径和方法同步到数据库的 API 列表中。
- 角色管理: 管理员在后台创建角色,例如“内容编辑”、“财务专员”等。
- 权限分配: 管理员进入“角色管理”界面,为某个角色(如“内容编辑”)勾选其可以访问的 API 列表(例如,勾选
GET /api/article/list
和POST /api/article/add
)。 - 保存生效: 保存后,
sys_role_api
表中会写入相应的关联记录。当一个角色为“内容编辑”的用户下次尝试访问/api/article/add
时,check_api_permission
就能查询到匹配记录,从而授权访问。
这套 RBAC 系统将权限管理的复杂性从代码中剥离,使其成为一个 动态的、可由业务人员配置的管理功能,极大地提升了系统的灵活性和安全性。