第二十六章:权限管理与路由守卫
博客v1.0系列教程(Dioxus)博客 v1.0 系列教程 (Dioxus)
第二十六章:权限管理与路由守卫
1. 权限模型
用户 ──→ 角色 ──→ 权限
│
┌──────────┴──────────┐
▼ ▼
页面级权限 组件级权限
(路由守卫) (按钮/操作)
三种常见的权限模型:
| 模型 | 说明 | 适用场景 | |------|------|---------| | RBAC(角色基) | 用户→角色→权限 | 博客后台管理 | | ABAC(属性基) | 根据用户/资源属性动态判断 | 多租户系统 | | 简单认证 | 仅区分 登录/未登录 | 个人博客基础需求 |
2. 认证状态管理
2.1 用户 Session 管理
use dioxus::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, PartialEq, Serialize, Deserialize)]
struct User {
id: i64,
username: String,
avatar: Option<String>,
role: UserRole,
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
enum UserRole {
Visitor, // 游客
Reader, // 读者
Author, // 作者
Admin, // 管理员
}
impl UserRole {
fn can_write(&self) -> bool {
matches!(self, UserRole::Author | UserRole::Admin)
}
fn can_manage(&self) -> bool {
matches!(self, UserRole::Admin)
}
}
// 认证上下文
#[derive(Clone, PartialEq)]
struct AuthContext {
user: Option<User>,
token: Option<String>,
}
fn use_auth() -> (
Signal<AuthContext>,
impl Fn(String, String), // login
impl Fn(), // logout
impl Fn() -> bool, // is_authenticated
) {
let ctx = use_signal(|| {
// 从 localStorage 恢复会话
let token = get_local_storage("auth_token");
let user = token.as_ref()
.and_then(|t| decode_token(t))
.and_then(|claims| fetch_user(&claims.sub).ok());
AuthContext { user, token }
});
let login = move |username: String, password: String| {
spawn(async move {
match login_api(&username, &password).await {
Ok((token, user)) => {
set_local_storage("auth_token", &token);
ctx.set(AuthContext {
user: Some(user),
token: Some(token),
});
}
Err(e) => {
console_log!("登录失败: {e}");
}
}
});
};
let logout = move || {
remove_local_storage("auth_token");
ctx.set(AuthContext { user: None, token: None });
};
let is_authenticated = move || ctx.read().user.is_some();
(ctx, login, logout, is_authenticated)
}
2.2 提供 Auth Context
#[component]
fn AuthProvider(children: Element) -> Element {
let ctx = use_signal(|| AuthContext { user: None, token: None });
use_context_provider(|| ctx);
// 初始化时从 token 恢复
use_effect(move || {
let token = get_local_storage("auth_token");
if let Some(t) = token {
// 验证 token 有效性
spawn(async move {
match verify_token(&t).await {
Ok(user) => {
ctx.write().user = Some(user);
ctx.write().token = Some(t);
}
Err(_) => {
// token 过期,清除
remove_local_storage("auth_token");
}
}
});
}
});
rsx! { {children} }
}
// 在 App 根组件使用
#[component]
fn App() -> Element {
rsx! {
AuthProvider {
Router::<Route> {}
}
}
}
3. 路由守卫
3.1 简单认证守卫
/// 需要登录的路由
#[component]
fn RequireAuth(children: Element) -> Element {
let auth = use_context::<Signal<AuthContext>>();
let navigator = use_navigator();
if auth.read().user.is_none() {
// 未登录 → 重定向到登录页,携带回跳地址
let current_path = web_sys::window()
.and_then(|w| w.location().pathname().ok())
.unwrap_or_default();
rsx! {
Redirect {
to: "/login?redirect={current_path}",
}
}
} else {
rsx! { {children} }
}
}
/// 需要特定角色的路由
#[component]
fn RequireRole(role: UserRole, children: Element) -> Element {
let auth = use_context::<Signal<AuthContext>>();
match &auth.read().user {
Some(user) if user.role as u8 >= role as u8 => {
rsx! { {children} }
}
Some(_) => rsx! {
div { class: "text-center py-16",
h2 { "权限不足" }
p { "你没有访问此页面的权限" }
a { href: "/", "返回首页" }
}
},
None => rsx! {
Redirect { to: "/login" }
},
}
}
// 在路由中使用
#[derive(Routable, Clone, PartialEq)]
enum Route {
#[route("/")]
Home {},
#[route("/blog")]
BlogList { series: Option<String>, category: Option<String> },
#[route("/login")]
Login {},
// 需要登录的路由
#[nest("/admin")]
#[layout(RequireAuth)]
#[route("/admin")]
AdminDashboard {},
#[route("/admin/articles")]
ArticleManager {},
#[route("/admin/articles/new")]
ArticleEditor { slug: Option<String> },
#[end_nest]
#[nest("/admin")]
#[layout(RequireRole { role: UserRole::Admin })]
#[route("/admin/users")]
UserManager {},
#[route("/admin/settings")]
SiteSettings {},
#[end_nest]
}
3.2 动态守卫
/// 基于属性的权限检查
#[component]
fn RequirePermission(
permission: &'static str,
fallback: Option<Element>,
children: Element,
) -> Element {
let auth = use_context::<Signal<AuthContext>>();
let has_perm = use_memo(move || {
check_permission(auth.read().user.as_ref(), permission)
});
if has_perm() {
rsx! { {children} }
} else {
rsx! { {fallback.unwrap_or_else(|| rsx! { div {} })} }
}
}
fn check_permission(user: Option<&User>, permission: &str) -> bool {
let Some(user) = user else { return false };
match permission {
"article:create" => user.role.can_write(),
"article:edit" => user.role.can_write(),
"article:delete" => user.role.can_manage(),
"user:manage" => user.role.can_manage(),
"comment:moderate" => user.role.can_manage(),
_ => false,
}
}
// 使用
RequirePermission {
permission: "article:create",
fallback: rsx! { p { "需要作者权限" } },
button { "新建文章" }
}
4. 登录页面
#[component]
fn LoginPage() -> Element {
let auth = use_context::<Signal<AuthContext>>();
let navigator = use_navigator();
let redirect = use_route().query::<LoginQuery>()
.unwrap_or_default()
.redirect
.unwrap_or_else(|| "/".to_string());
let username = use_signal(String::new);
let password = use_signal(String::new);
let error = use_signal(|| Option::<String>::None);
let loading = use_signal(|| false);
let do_login = move |_| {
if username().is_empty() || password().is_empty() {
error.set(Some("请输入用户名和密码".to_string()));
return;
}
loading.set(true);
error.set(None);
spawn({
let u = username();
let p = password();
let nav = navigator.clone();
let redirect = redirect.clone();
async move {
match login_api(&u, &p).await {
Ok((token, user)) => {
set_local_storage("auth_token", &token);
auth.write().set(AuthContext {
user: Some(user),
token: Some(token),
});
nav.push(&redirect);
}
Err(msg) => {
error.set(Some(msg));
}
}
loading.set(false);
}
});
};
rsx! {
div { class: "max-w-sm mx-auto mt-16",
h1 { class: "text-xl font-bold mb-6 text-center", "登录" }
if let Some(ref msg) = error() {
div { class: "bg-red-100 text-red-700 px-4 py-2 rounded mb-4 text-sm",
"{msg}"
}
}
form { onsubmit: do_login, class: "space-y-4",
div {
label { class: "block text-sm font-medium mb-1", "用户名" }
input {
class: "w-full border rounded-lg px-3 py-2",
value: "{username}",
oninput: move |e| username.set(e.value()),
}
}
div {
label { class: "block text-sm font-medium mb-1", "密码" }
input {
r#type: "password",
class: "w-full border rounded-lg px-3 py-2",
value: "{password}",
oninput: move |e| password.set(e.value()),
}
}
button {
class: "w-full py-2 rounded-lg text-white font-medium",
style: "background: var(--primary);",
disabled: loading(),
if loading() { "登录中..." } else { "登录" }
}
}
}
}
}
5. JWT Token 管理
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct JwtClaims {
sub: String, // 用户 ID
exp: usize, // 过期时间
iat: usize, // 签发时间
role: String, // 角色
}
fn decode_token(token: &str) -> Option<JwtClaims> {
// 注意:前端只解码 payload,不验证签名(由服务端验证)
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 { return None; }
use base64::Engine;
let payload = base64::engine::general_purpose::URL_SAFE
.decode(parts[1]).ok()?;
serde_json::from_slice(&payload).ok()
}
fn is_token_expired(token: &str) -> bool {
decode_token(token)
.map(|claims| {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as usize;
claims.exp <= now
})
.unwrap_or(true)
}
// 自动刷新 Token
fn use_auto_refresh() {
let auth = use_context::<Signal<AuthContext>>();
use_interval(300_000, move || { // 每 5 分钟检查
let token = auth.read().token.clone();
if let Some(t) = token {
if is_token_expired(&t) {
spawn(async move {
match refresh_token(&t).await {
Ok(new_token) => {
set_local_storage("auth_token", &new_token);
auth.write().token = Some(new_token);
}
Err(_) => {
// 刷新失败,强制登出
remove_local_storage("auth_token");
auth.write().user = None;
auth.write().token = None;
}
}
});
}
}
});
}
6. API 请求携带 Token
fn use_authenticated_client() -> reqwest::Client {
let auth = use_context::<Signal<AuthContext>>();
let mut headers = reqwest::header::HeaderMap::new();
if let Some(token) = &auth.read().token {
headers.insert(
reqwest::header::AUTHORIZATION,
reqwest::header::HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
);
}
reqwest::Client::builder()
.default_headers(headers)
.build()
.unwrap()
}
async fn fetch_with_auth(url: &str) -> Result<serde_json::Value, String> {
let client = use_authenticated_client();
let resp = client.get(url).send().await.map_err(|e| e.to_string())?;
match resp.status() {
reqwest::StatusCode::UNAUTHORIZED => {
// Token 过期,清除本地会话
remove_local_storage("auth_token");
Err("未授权,请重新登录".to_string())
}
reqwest::StatusCode::FORBIDDEN => {
Err("权限不足".to_string())
}
status if status.is_success() => {
resp.json().await.map_err(|e| e.to_string())
}
_ => {
Err(format!("请求失败: {}", resp.status()))
}
}
}
7. 组件级权限控制
#[component]
fn AdminActions(article: ArticleSummary) -> Element {
let auth = use_context::<Signal<AuthContext>>();
// 判断当前用户是否有权操作此文章
let can_edit = use_memo(move || {
let user = auth.read().user.as_ref()?;
Some(user.id == article.author_id || user.role.can_manage())
});
rsx! {
div { class: "flex gap-2 mt-4",
if can_edit() == Some(true) {
button { class: "px-3 py-1 rounded text-sm border", "编辑" }
button { class: "px-3 py-1 rounded text-sm border text-red-500", "删除" }
}
}
}
}
// 权限组件抽象
#[component]
fn IfGranted(
permission: &'static str,
children: Element,
) -> Element {
let auth = use_context::<Signal<AuthContext>>();
let has_perm = use_memo(move || {
check_permission(auth.read().user.as_ref(), permission)
});
if has_perm() { rsx! { {children} } }
else { rsx! { div {} } }
}
// 使用:只有管理员能看到
IfGranted {
permission: "user:manage",
button { "管理用户" }
}
8. 登录后重定向
#[component]
fn LoginRedirect() -> Element {
let auth = use_context::<Signal<AuthContext>>();
let navigator = use_navigator();
use_effect(move || {
if auth.read().user.is_some() {
// 已登录 → 跳转到之前的页面或首页
let redirect = use_route().query::<LoginQuery>()
.ok()
.and_then(|q| q.redirect)
.unwrap_or_else(|| "/".to_string());
navigator.push(&redirect);
}
});
rsx! { div {} }
}
9. 完整权限系统集成
// 在 App 根组件中组装
#[component]
fn App() -> Element {
rsx! {
AuthProvider {
Router::<Route> {}
}
}
}
// 路由配置示例
#[derive(Routable, Clone, PartialEq)]
enum Route {
#[route("/")]
Home {},
#[route("/blog")]
BlogList { series: Option<String>, category: Option<String> },
#[route("/article/:slug")]
BlogPost { slug: String },
#[route("/login")]
LoginPage,
// 管理后台 —— 需要登录
#[layout(AdminLayout)]
#[nest("/admin")]
#[layout(RequireAuth)]
#[route("/admin")]
AdminDashboard {},
#[route("/admin/articles")]
ArticleManager {},
#[end_nest]
#[layout(RequireRole { role: UserRole::Admin })]
#[route("/admin/users")]
UserManager {},
#[end_nest]
#[end_nest]
#[route("/:404")]
NotFound {},
}
10. 小结
use_context+ Signal 实现全局认证状态管理- 路由守卫组件包裹需要保护的路由,未认证时自动重定向
RequireRole守卫实现基于角色的访问控制RequirePermission守卫实现细粒度的权限检查- 组件内部使用
IfGranted控制按钮/操作的可见性 - JWT Token 自动刷新机制保持会话持续
- API 请求携带 Bearer Token,处理 401/403 响应
- 登录成功后根据
redirect参数跳转回原始页面
dioxusauthpermissionroute-guardroleaccess-control