十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Leptos + Axum 会话认证实战:session_auth_axum 示例深度解析

Leptos + Axum 会话认证实战:session_auth_axum 示例深度解析 Leptos Axum 会话认证实战session_auth_axum 示例深度解析【免费下载链接】leptosBuild fast web applications with Rust.项目地址: https://gitcode.com/GitHub_Trending/le/leptos导读本文以 Leptos 仓库中的 session_auth_axum 示例为对象完整拆解一个基于 Rust 的全栈 Web 应用它使用 Leptos 构建前端视图以 Axum 作为 HTTP 后端通过 Leptos 服务端函数Server Functions在浏览器中直接调用运行于服务端的sqlx数据库操作并借助axum_session/axum_session_auth实现登录、注册、登出与基于会话的权限控制。读完本文你将掌握如何用cargo leptos watch一键启动这类 SSR 客户端水合应用、如何配置 Axum 会话存储与认证中间件、如何用服务端函数把数据库读写安全地暴露给前端以及如何用ProtectedRoute保护受权限约束的路由。示例概览一个带用户体系的 Todo 应用该示例是一个功能完整的 Todo 应用访客可以浏览任务用户可以注册、登录后提交属于自己的任务并进入受保护的设置页登出。整体技术栈与架构如下前端框架Leptos含 leptos_meta 与 leptos_router采用 SSR 客户端水合hydration模式HTTP 服务Axum通过 leptos_axum 集成层生成路由并处理静态资源数据库SQLite使用sqlx异步访问建表脚本位于 migrations/20230226000000_create_todo_table.sql会话与认证axum_session、axum_session_auth与axum_session_sqlx会话数据持久化到 SQLite密码安全bcrypt哈希存储绝不保存明文密码。从源码结构看示例的模块划分清晰各文件职责单一src/main.rsAxum 服务入口负责数据库连接池、会话/认证中间件装配与路由挂载src/lib.rs声明模块并定义 WASM 水合入口hydrate()src/state.rs基于 AxumFromRef的共享AppStatesrc/auth.rs用户模型、Authentication/HasPermissiontrait 实现及登录注册登出服务端函数src/todo.rsTodo 数据模型、Todo 相关服务端函数与全部 UI 组件src/errors.rs 与 src/error_template.rs错误类型与统一错误展示组件。快速启动两条运行路径方式一cargo leptos推荐README 明确给出了最快捷的启动方式cargo leptos watch该命令依赖 cargo-leptos 中[package.metadata.leptos]段的配置以site-addr 127.0.0.1:3000作为服务监听地址将编译产物输出到target/site并把 style.css 编译为pkg/session_auth_axum.css。启动后浏览器访问 http://127.0.0.1:3000 即可。开发模式下watch false时shell 组件 会通过AutoReload与HydrationScripts注入热更新与水合脚本。方式二cargo make按照 examples/README.md 的通用流程也可以使用cargo-make运行进入示例目录并安装工具链cargo install cargo-make并确保rustup target add wasm32-unknown-unknown已执行运行cargo make ci完成依赖与检查运行cargo make start启动服务默认端口见控制台输出运行cargo make stop结束进程。该示例的 Makefile.toml 扩展自 examples/cargo-make/main.toml 与 cargo-make/cargo-leptos.toml并以CLIENT_PROCESS_NAME session_auth_axum标识客户端进程。环境前提按 examples/README.md 的说明运行示例需要Rust 工具链Nightly、wasm32-unknown-unknown目标、cargo-leptos或cargo-make数据库无需手动初始化——服务启动时sqlx::migrate!()会自动执行迁移创建三张表。Cargo.toml 拆解feature 开关与数据库选型Cargo.toml 是本示例的配置核心值得逐段解读。双目标产物[lib] crate-type [cdylib, rlib]cdylib用于编译为 WASM 供浏览器加载rlib用于服务端原生编译这是 Leptos SSR 应用的标准配置。按 feature 拆分的依赖服务端专属依赖axum、tokio、sqlx、bcrypt、axum_session*等全部标记为optional true并归入ssrfeature[features] default [ssr] hydrate [leptos/hydrate] ssr [ dep:axum, dep:tower, dep:tower-http, dep:tokio, dep:axum_session_sqlx, dep:axum_session_auth, dep:axum_session, dep:async-trait, dep:sqlx, dep:bcrypt, dep:rand, leptos/ssr, leptos_meta/ssr, leptos_router/ssr, dep:leptos_axum, ]default [ssr]默认按 SSR 模式构建二进制hydrate客户端水合模式仅启用leptos/hydrate服务端依赖全部关闭[package.metadata.leptos]中bin-features [ssr]、lib-features [hydrate]即二进制编译为服务端版本库编译为客户端水合版本二者通过 src/lib.rs 中的hydrate()入口衔接。SQLite 与依赖版本数据库访问基于sqlx0.8启用runtime-tokio-rustls与sqlitefeature会话持久化由axum_session_sqlx提供启用sqlite、tls-rustls。此外还依赖serde用户/Todo 序列化、thiserror错误定义、console_log与console_error_panic_hookWASM 端日志与 panic 追踪。服务端装配会话、认证中间件与路由src/main.rs 演示了完整的 Axum 服务装配流程。数据库连接池与自动迁移let pool SqlitePoolOptions::new() .connect(sqlite:Todos.db) .await .expect(Could not make pool.); // ... if let Err(e) sqlx::migrate!().run(pool).await { eprintln!({e:?}); }连接串sqlite:Todos.db对应仓库根目录下的 Todos.db首次运行自动生成。sqlx::migrate!()以内嵌宏方式读取 migrations 目录并逐条执行无需外部迁移工具。会话存储与认证配置let session_config SessionConfig::default().with_table_name(axum_sessions); let auth_config AuthConfig::i64::default(); let session_store SessionStore::SessionSqlitePool::new( Some(SessionSqlitePool::from(pool.clone())), session_config, ) .await .unwrap();SessionConfig指定会话表名为axum_sessions默认值可覆盖AuthConfig::i64的泛型参数即用户主键类型与后面User.id: i64对应SessionStore::SessionSqlitePool将会话数据持久化到同一个 SQLite 连接池服务重启后会话仍可恢复。路由与中间件叠加let app_state AppState { leptos_options, pool: pool.clone(), routes: routes.clone() }; let app Router::new() .leptos_routes(app_state, routes, move || shell(options.clone())) .fallback(leptos_axum::file_and_error_handler::AppState, _(shell)) .layer( AuthSessionLayer::User, i64, SessionSqlitePool, SqlitePool::new( Some(pool.clone()), ) .with_config(auth_config), ) .layer(SessionLayer::new(session_store)) .with_state(app_state);调用链如下generate_route_list(TodoApp)扫描组件树生成 Leptos 路由列表leptos_routes注册 SSR 路由fallback兜底静态资源与 404AuthSessionLayer解析会话中的用户注入AuthSessionUser, i64, ...提取器SessionLayer处理会话读写with_state注入AppState。最后tokio::net::TcpListener::bind(addr)绑定site_addr默认 127.0.0.1:3000并通过axum::serve提供服务。AppState多字段共享状态的正确姿势src/state.rs 通过 Axum 的FromRef派生宏把三个字段合并进统一状态类型#[derive(FromRef, Debug, Clone)] pub struct AppState { pub leptos_options: LeptosOptions, pub pool: SqlitePool, pub routes: VecAxumRouteListing, }源码注释解释了原因Leptos 的路由处理器要求AppState中包含leptos_options而应用同时还需要连接池与路由列表FromRef是 Axum SubStates 特性下携带多个状态项的唯一方式。服务端函数内部则通过with_context::AppState, _(|state| state.pool.clone())从 Leptos 上下文中取出连接池见 src/todo.rs。认证模型用户、密码哈希与权限User 结构与 Guest 默认值src/auth.rs 定义了核心用户类型#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct User { pub id: i64, pub username: String, pub permissions: HashSetString, } impl Default for User { fn default() - Self { Self { id: -1, username: Guest.into(), permissions: HashSet::new() } } }注意两点设计细节User实现了Serialize/Deserialize因为它要通过服务端函数返回给前端展示get_user返回OptionUser访客会话中不存在User前端展示时用user.unwrap_or_default()兜底为Guest。密码哈希隔离UserPasshash#[derive(Clone, Debug, PartialEq, Eq)] pub struct UserPasshash(String);源码注释明确写着“Explicitly is not Serialize/Deserialize!”。UserPasshash刻意不实现序列化确保密码哈希永远不会随User一起泄露给客户端。它只在服务端verify(password, expected_passhash)校验时使用。认证与权限 trait 实现在#[cfg(feature ssr)]模块内AuthenticationUser, i64, SqlitePool for User实现load_user按 id 查库并注入权限集合、is_authenticated、is_active、is_anonymousHasPermissionSqlitePool for Userhas()直接检查self.permissions.contains(perm)权限 token 来自user_permissions表。查询辅助方法User::get_with_passhash执行两条 SQL先按 id 查users再查user_permissions取出全部权限 token最后经SqlUser::into_user组装成(User, UserPasshash)元组。get_from_username_with_passhash同理按用户名查询供登录校验使用。服务端函数登录、注册与登出的完整闭环服务端函数Server Function是 Leptos 的核心机制前端像调用本地函数一样发起请求代码却在服务端执行。src/auth.rs 中的三个#[server]函数构成了完整的认证闭环。登录Login#[server(Login, /api)] pub async fn login( username: String, password: String, remember: OptionString, ) - Result(), ServerFnError { let pool pool()?; let auth auth().await?; let (user, UserPasshash(expected_passhash)) User::get_from_username_with_passhash(username, pool) .await .ok_or_else(|| ServerFnError::new(User does not exist.))?; match verify(password, expected_passhash)? { true { auth.login_user(user.id); auth.remember_user(remember.is_some()); leptos_axum::redirect(/); Ok(()) } false Err(ServerFnError::ServerError(Password does not match..to_string())), } }要点auth()通过leptos_axum::extract()获取当前请求的AuthSession见 src/todo.rs。extract是 integrations/axum/src/lib.rs 提供的辅助函数对任意实现FromRequestParts的类型做提取并统一转换为ServerFnError密码校验使用bcrypt::verify登录成功即login_user写入会话remember复选框决定是否使用持久会话leptos_axum::redirect(/)让浏览器在成功/失败后跳回首页。注册Signup#[server(Signup, /api)] pub async fn signup( username: String, password: String, password_confirmation: String, remember: OptionString, ) - Result(), ServerFnError { if password ! password_confirmation { return Err(ServerFnError::ServerError(Passwords did not match..to_string())); } let password_hashed hash(password, DEFAULT_COST).unwrap(); sqlx::query(INSERT INTO users (username, password) VALUES (?,?)) .bind(username.clone()) .bind(password_hashed) .execute(pool) .await?; let user User::get_from_username(username, pool) .await .ok_or_else(|| ServerFnError::new(Signup failed: User does not exist.))?; auth.login_user(user.id); auth.remember_user(remember.is_some()); leptos_axum::redirect(/); Ok(()) }注册流程先做两次密码一致性校验再以bcrypt::hash(password, DEFAULT_COST)计算哈希入库DEFAULT_COST是 bcrypt 默认计算成本随后立即查询该用户并自动登录。登出Logout#[server(Logout, /api)] pub async fn logout() - Result(), ServerFnError { let auth auth().await?; auth.logout_user(); leptos_axum::redirect(/); Ok(()) }logout_user()清除会话中的用户标识配合leptos_axum::redirect(/)回到首页。当前用户查询#[server] pub async fn get_user() - ResultOptionUser, ServerFnError { let auth auth().await?; Ok(auth.current_user) }auth.current_user即当前会话用户未登录时为None。前端通过Resource监听登录/注册/登出动作的版本号变化并自动重新拉取实现用户状态的响应式刷新见 src/todo.rs。Todo 数据层服务端函数 sqlx数据模型src/todo.rs 中Todo直接内嵌OptionUser方便前端直接显示“由谁创建”。对应的SqlTodosqlx::FromRow派生负责从todos表按行读取再通过into_todo异步补全用户信息。查询全部GetTodos#[server(GetTodos, /api)] pub async fn get_todos() - ResultVecTodo, ServerFnError { let pool pool()?; Ok(join_all( sqlx::query_as::_, SqlTodo(SELECT * FROM todos) .fetch_all(pool) .await? .iter() .map(|todo: SqlTodo| todo.clone().into_todo(pool)), ) .await) }这里用futures::future::join_all并发地把每一行转换为带用户信息的Todo。新增AddTodo#[server(AddTodo, /api)] pub async fn add_todo(title: String) - Result(), ServerFnError { let user get_user().await?; let pool pool()?; let id match user { Some(user) user.id, None -1 }; // fake API delay std::thread::sleep(std::time::Duration::from_millis(1250)); Ok(sqlx::query( INSERT INTO todos (title, user_id, completed) VALUES (?, ?, false), ) .bind(title).bind(id).execute(pool).await.map(|_| ())?) }新增任务时读取当前用户未登录则以-1作为user_id对应 UI 中的 Guest 兜底std::thread::sleep(1250ms)是刻意加入的“伪 API 延迟”用于演示ServerMultiAction的 pending 提交状态。删除DeleteTodo#[server] pub async fn delete_todo(id: u16) - Result(), ServerFnError { let pool pool()?; Ok(sqlx::query(DELETE FROM todos WHERE id $1) .bind(id).execute(pool).await.map(|_| ())?) }源码注释表明#[server]的“结构体名与路径前缀参数都是可选的”这里省略后由函数名自动生成。数据库表结构migrations/20230226000000_create_todo_table.sql 定义三张表usersid自增主键、username唯一、password存放 bcrypt 哈希、created_at默认当前时间戳user_permissionsuser_id与权限token的关联表todosuser_id关联用户completed布尔标记created_at默认当前时间戳。文件中的注释还保留着“Guest 匿名用户种子数据”的备选写法当前被注释掉以及可选的FOREIGN KEY ... ON DELETE CASCADE外键约束说明。前端响应式层ServerAction、Resource 与 ProtectedRoute组件树与头部状态栏TodoApp 是根组件它创建三个ServerActionlet login ServerAction::Login::new(); let logout ServerAction::Logout::new(); let signup ServerAction::Signup::new(); let user Resource::new( move || (login.version().get(), signup.version().get(), logout.version().get()), move |_| get_user(), );Resource的依赖是三个 action 的version()信号——任何一次登录/注册/登出都会使版本号变化并触发get_user()重新执行从而自动刷新头部显示“Logged in as: xxx (id)”。Transition包裹该区域加载中显示Loading...get_user返回错误时头部还会渲染Login error: {e}提示。路由与受保护路由FlatRoutes fallback|| Not found. Route pathpath!() viewTodos/ Route pathpath!(signup) viewmove || view! { Signup actionsignup/ }/ Route pathpath!(login) viewmove || view! { Login actionlogin/ }/ ProtectedRoute pathpath!(settings) conditionmove || user.get().map(|r| r.ok().flatten().is_some()) redirect_path|| / viewmove || { view! { h1Settings/h1 Logout actionlogout/ } } / /FlatRoutes/settings使用ProtectedRoutecondition判断当前user是否为Some未登录自动redirect_path回/三个认证 action 分别绑定到登录、注册、登出页面。表单与提交状态Login/Signup/Logout组件均以ActionForm包裹表单字段名username、password、password_confirmation、remember与服务端函数的参数一一对应Todos 列表使用ServerMultiAction::AddTodosubmissions()返回进行中的提交集合pending()信号驱动“提交中的 Todo”渲染为灰色pending列表项配合 1250ms 的模拟延迟直观展示乐观更新效果ServerAction::DeleteTodo通过ActionForm的隐藏字段id提交删除。错误边界列表区域包在ErrorBoundary中error_template.rs 的ErrorTemplate会从错误信号中downcast_ref::TodoAppError()过滤出应用自定义错误在 SSR 端通过ResponseOptions把第一个错误的 HTTP 状态码写入响应NotFound→ 404InternalServerError→ 500见 errors.rs用For循环渲染出错误码与错误文本列表。与姊妹示例的对比从 todo_app_sqlite 到完整认证若将本示例与 todo_app_sqlite_axum 对照阅读可清晰看出认证能力的增量后者只有todos单表与匿名 Todo CRUD前者则新增users、user_permissions两张表并叠加会话中间件、bcrypt 密码校验、ProtectedRoute路由守卫与AuthSession提取器。这也解释了为什么本示例被归类在 projects 目录——它比examples下的教学示例更接近生产形态。常见问题与排错思路端口冲突site-addr 127.0.0.1:3000、reload-port 3001若被占用可在 Cargo.toml 中调整后重新cargo leptos watch迁移失败sqlx::migrate!()要求migrations目录文件名格式为version_name.sql本示例为20230226000000_create_todo_table.sql删除旧的Todos.db可让迁移重新执行客户端 404 或样式丢失确认assets-dir public下的 favicon.ico 与pkg目录已随构建产物拷贝到target/siteshell 中引用的/pkg/session_auth_axum.css路径与output-name一致登录后仍显示 Logged out检查SessionStore的持久化表名是否与数据库实际表一致并确认AuthSessionLayer与SessionLayer的叠加顺序正确认证层在会话层之上密码不匹配注册与登录使用同一bcrypt版本bcrypt 0.17与DEFAULT_COST跨版本升级后旧哈希可能出现校验失败。小结session_auth_axum是一个麻雀虽小五脏俱全的全栈认证参考实现它把 Leptos 的响应式前端、Axum 的中间件体系、sqlx 的异步 SQLite 访问和 bcrypt 密码安全整合进同一个 crate并通过 feature 开关优雅地分离了 SSR 与水合构建。以此为模板你可以快速迁移出自己的用户系统——只需要替换 auth.rs 中的用户模型与查询、调整 migrations 中的表结构并把 main.rs 中的连接池替换为 PostgreSQL/MySQLaxum_session_sqlx与sqlx均已预留对应 feature 空间即可。【免费下载链接】leptosBuild fast web applications with Rust.项目地址: https://gitcode.com/GitHub_Trending/le/leptos创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表