feat(admin): SSR 层认证守卫,未登录直接跳登录页
此前后台鉴权完全在客户端 WASM 完成:SSR 渲染骨架屏 → 浏览器下载 /编译 WASM → hydrate → 异步 get_current_user() → 客户端 navigator. push(Login)。整条链串行,未登录用户首屏要空白好久才跳转登录。 新增 admin_guard 中间件:/admin* 请求在进入 Dioxus SSR 渲染器之前 读 session cookie → get_user_by_token 校验 admin 角色 → 失败直接 302 到 /login。未登录用户首屏即登录页,零 SSR 渲染开销。 - 置于 app_routes layer 链最外层(CSRF/cache/SSR 之前短路) - 复用 get_user_by_token:命中内存缓存 + session_generation 校验, 与客户端鉴权同一套语义 - DB 错误 fail-open(避免抖动把已登录管理员踢登录页),客户端 AdminLayout 仍作兜底 - /admin 与 /login 本就不进 SSR 缓存,302 不会被缓存
This commit is contained in:
parent
16bd644783
commit
58f1a981d3
61
src/main.rs
61
src/main.rs
@ -209,6 +209,63 @@ async fn add_cache_control(
|
||||
response
|
||||
}
|
||||
|
||||
/// Axum 中间件:`/admin*` 的 SSR 层认证守卫。
|
||||
///
|
||||
/// 未登录访问后台时,服务端**直接 302 跳转 `/login`**,根本不进入 Dioxus
|
||||
/// SSR 渲染器。此前后台鉴权完全在客户端 WASM 完成(SSR 渲染骨架屏 → WASM
|
||||
/// 下载/编译 → hydrate → 异步 `get_current_user()` → 客户端 `navigator.push`),
|
||||
/// 整条链串行,未登录用户首屏要"空白好久"才跳登录。
|
||||
///
|
||||
/// - 只匹配 `/admin*`,其它路径(`/login`、公开页、`/api/*`)直接放行。
|
||||
/// - 复用 `get_user_by_token`:命中内存缓存 + 校验 `session_generation`
|
||||
/// (封禁/降级后旧 session 立即失效),与客户端鉴权同一套语义。
|
||||
/// - DB 错误时 **fail-open**(放行进入 SSR):避免数据库抖动把已登录的
|
||||
/// 管理员也踢到登录页;客户端 `AdminLayout` 仍有兜底校验。
|
||||
/// - `/admin` 与 `/login` 本就不进 `cache_control_for_path` 缓存,302 不会被缓存。
|
||||
#[cfg(feature = "server")]
|
||||
async fn admin_guard(
|
||||
req: axum::extract::Request,
|
||||
next: axum::middleware::Next,
|
||||
) -> axum::response::Response {
|
||||
use axum::body::Body;
|
||||
use axum::http::{header, StatusCode};
|
||||
use axum::response::Response;
|
||||
use crate::models::user::UserRole;
|
||||
|
||||
let path = req.uri().path().to_string();
|
||||
if !path.starts_with("/admin") {
|
||||
return next.run(req).await;
|
||||
}
|
||||
|
||||
// 从 Cookie 头读 session token(与 export.rs / upload.rs 同款手法)。
|
||||
let cookie = req
|
||||
.headers()
|
||||
.get("cookie")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.unwrap_or("");
|
||||
let token = crate::auth::session::parse_session_token(cookie);
|
||||
|
||||
let is_admin = match token {
|
||||
Some(t) => match crate::api::auth::get_user_by_token(t).await {
|
||||
Ok(Some(user)) => user.role == UserRole::Admin,
|
||||
// Err(DB 抖动)/ Ok(None)(token 无效):fail-open,交给客户端兜底。
|
||||
_ => true,
|
||||
},
|
||||
// 无 token:明确未登录,拦截。
|
||||
None => false,
|
||||
};
|
||||
|
||||
if is_admin {
|
||||
next.run(req).await
|
||||
} else {
|
||||
Response::builder()
|
||||
.status(StatusCode::FOUND)
|
||||
.header(header::LOCATION, "/login")
|
||||
.body(Body::empty())
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// 程序入口
|
||||
fn main() {
|
||||
// server feature:启动服务端
|
||||
@ -425,6 +482,10 @@ fn main() {
|
||||
StatusCode::REQUEST_TIMEOUT,
|
||||
Duration::from_secs(30),
|
||||
));
|
||||
// admin_guard 置于最外层(最后添加 = 最先执行):未登录的 /admin* 请求
|
||||
// 在 CSRF / cache / SSR 渲染之前就被 302 短路,零渲染开销。
|
||||
let app_routes =
|
||||
app_routes.layer(axum::middleware::from_fn(admin_guard));
|
||||
|
||||
// 静态资源路由:图片文件服务。
|
||||
// 注意:`dioxus::server::serve()` 接管了 listener 与 `into_make_service`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user