API 权限检查
大约 3 分钟
API 权限检查
基于“角色-API-方法”的精确匹配模型,在中间件中完成权限验证。整体流程:
- 记录请求日志(含请求体截断)
- 生成请求上下文 ReqCtx(规范化 path、method)
- 校验当前用户 UserInfo 对目标 API 的访问权限(支持超管白名单)
- 放行或返回无权限提示(安全起见默认 404)
架构与流程
中间件链
- request_log_fn_mid:记录 method、path、query、UA、Content-Type、body(最大 1000 字符,超长截断)
- auth_fn_mid:构建并注入 ReqCtx(含去掉 /api 前缀的路径、查询串、方法等)
- api_fn_mid:从扩展中取 UserInfo、ReqCtx,调用权限服务 check_api_permission
权限模型
- 表:sys_role_api(或同义)
- 维度:role_id + api + method 完全匹配
- 超级角色:APPCOFIG.system.super_role 列表内的角色无条件放行
核心结构与函数
请求上下文 ReqCtx
#[derive(Clone, Debug, Default)]
pub struct ReqCtx {
pub ori_uri: String, // 原始 URI(含 /api + query)
pub path: String, // 去掉 /api 前缀的路由路径
pub path_params: String, // 原始 query
pub method: String, // HTTP 方法
}
鉴权中间件(节选,已为你实现)
pub async fn auth_fn_mid(req: Request, next: Next)
-> Result<impl IntoResponse, (StatusCode, String)> {
let ori_uri_path = if let Some(path) = req.extensions().get::<OriginalUri>() {
path.0.path().to_owned()
} else {
req.uri().path().to_owned()
};
let path = ori_uri_path.replacen("/api", "", 1);
let method = req.method().to_string();
let path_params = req.uri().query().unwrap_or("").to_string();
// 读取并重建请求体,避免后续 handler 读不到
let (parts, body) = req.into_parts();
let bytes = body.collect().await.unwrap().to_bytes();
let req_ctx = ReqCtx {
ori_uri: if path_params.is_empty() { ori_uri_path } else { format!("{}?{}", ori_uri_path, path_params) },
path,
path_params,
method,
};
let mut req = Request::from_parts(parts, Body::from(bytes));
req.extensions_mut().insert(req_ctx);
Ok(next.run(req).await)
}
权限校验中间件(节选)
pub async fn api_fn_mid(req: Request, next: Next)
-> Result<Response, (StatusCode, String)> {
let ctx = req.extensions().get::<ReqCtx>().expect("ReqCtx not found");
let user = req.extensions().get::<UserInfo>().expect("UserInfo not found");
let ok = s_sys_role_api::check_api_permission(user.rid, &ctx.path, &ctx.method).await;
if ok {
Ok(next.run(req).await)
} else {
tracing::info!("没有API权限 {:?} -> {} {}", user, ctx.method, ctx.path);
let body = Json(serde_json::json!({ "message": "没有API权限" }));
// 出于安全考虑返回 404(隐藏接口存在性),需要明确拒绝可改 403
Err((StatusCode::NOT_FOUND, body.to_string()))
}
}
请求日志中间件(节选)
pub async fn request_log_fn_mid(req: Request, next: Next)
-> Result<impl IntoResponse, (StatusCode, String)> {
let (parts, body) = req.into_parts();
let method = parts.method.to_string();
let uri = parts.uri.clone();
let path = uri.path();
let query = uri.query().unwrap_or("");
let ua = parts.headers.get(axum::http::header::USER_AGENT)
.map_or("", |h| h.to_str().unwrap_or(""));
let ct = parts.headers.get(axum::http::header::CONTENT_TYPE)
.map_or("", |h| h.to_str().unwrap_or(""));
let body_bytes = axum::body::to_bytes(body, usize::MAX).await
.map_err(|e| (StatusCode::BAD_REQUEST, format!("读取请求体失败: {}", e)))?;
let body_preview = {
let s = String::from_utf8_lossy(&body_bytes);
if s.len() > 1000 { format!("{}...(truncated)", &s[..1000]) } else { s.to_string() }
};
tracing::info!("http-request method:{} url:{} path:{} query:{} ua:{} ct:{} body_size:{} body:{}",
method, uri, path, query, ua, ct, body_bytes.len(), body_preview);
Ok(next.run(Request::from_parts(parts, Body::from(body_bytes))).await)
}
权限服务与数据访问
服务入口(超管绕过 + DB 精确匹配)
pub async fn check_api_permission(rid: i64, api: &str, method: &str) -> bool {
if APPCOFIG.system.super_role.contains(&rid) {
return true;
}
let arg = RoleApiCheckInfo { role_id: rid, api: api.to_owned(), method: method.to_owned() };
SysRoleApiModel::check_api(arg).await
}
DAO(基于 SeaORM,按 role_id + api + method 精确查询)
/// 检查权限:存在即有权
pub async fn check_api(arg: RoleApiCheckInfo) -> bool {
tracing::info!("check_api: {:?}", arg);
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()
}
辅助接口(节选)
- role_permission_list(rid):返回角色拥有的 API 列表
- role_api_transfer_list(req):权限穿梭框数据源
- add_many_role_api_transfer(req):批量赋权
注意:auth_fn_mid 会把 path 处理为“去掉 /api 前缀”的路径,因此 DB 中的 api 字段也应存“无 /api 前缀”的版本,确保一致性。
数据规范与填充
- method:存大写(GET/POST/PUT/DELETE/PATCH...)与 ctx.method 保持一致
- api:存去掉 /api 前缀的路由路径,例如:
- 请求路径:/api/sys/role/list → 存 /sys/role/list
- 超级角色:配置于 APPCOFIG.system.super_role,匹配 rid 即放行
- 迁移/初始化:可提供“全部接口扫描入库 + UI 选择授权”的流程,或按需手工添加
返回码策略
- 当前实现:无权限返回 404 + {"message":"没有API权限"},用来隐藏接口存在性,降低探测面