From 0be0719fdbfb67ff53efb5d966423a477efd3672 Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 26 May 2026 23:54:58 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=86=20session=20cookie=20=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=90=8E=E7=AB=AF=20HttpOnly=20=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - login 通过 Set-Cookie 响应头设置 HttpOnly cookie - get_current_user 从请求 Cookie header 读取 token,匹配具体 session - logout 通过 Set-Cookie 清除 cookie,并删除对应 session - 移除前端 document.cookie 操作代码 Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 1 + Cargo.toml | 1 + src/api/auth.rs | 84 ++++++++++++++++++++++++++++++---- src/components/admin_layout.rs | 13 ------ src/pages/login.rs | 18 -------- 5 files changed, 78 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5e0d1b2..3d892dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4629,6 +4629,7 @@ dependencies = [ "dioxus", "dotenvy", "getrandom 0.2.17", + "http", "js-sys", "pulldown-cmark", "rand 0.8.6", diff --git a/Cargo.toml b/Cargo.toml index 470bc0d..ad2175c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ pulldown-cmark = "0.13" dotenvy = { version = "0.15", optional = true } rand = { version = "0.8", features = ["getrandom"] } getrandom = { version = "0.2", features = ["js"] } +http = "1" [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys = { version = "0.3", features = ["Document", "Window", "HtmlDocument", "Storage", "Element", "DomTokenList", "MediaQueryList", "HtmlScriptElement"] } diff --git a/src/api/auth.rs b/src/api/auth.rs index c27768b..efcb751 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -1,6 +1,8 @@ #![allow(clippy::unused_unit, deprecated, unused_imports)] use dioxus::prelude::*; +#[cfg(feature = "server")] +use http::header::{HeaderValue, SET_COOKIE}; use crate::auth::{password, session}; use crate::db::pool::DB_POOL; @@ -34,6 +36,23 @@ fn validate_password(password: &str) -> Result<(), String> { Ok(()) } +#[cfg(feature = "server")] +fn parse_session_token(cookie_header: &str) -> Option<&str> { + cookie_header + .split(';') + .map(|s| s.trim()) + .find_map(|pair| { + let mut parts = pair.splitn(2, '='); + let name = parts.next()?.trim(); + let value = parts.next()?.trim(); + if name == "session" { + Some(value) + } else { + None + } + }) +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct AuthResponse { pub success: bool, @@ -171,6 +190,16 @@ pub async fn login( .await .map_err(|e| ServerFnError::new(format!("创建 session 失败: {}", e)))?; + let cookie = format!( + "session={token}; HttpOnly; Path=/; Max-Age={}; SameSite=Lax", + 30 * 24 * 60 * 60 + ); + if let Some(ctx) = dioxus::fullstack::FullstackContext::current() { + if let Ok(value) = HeaderValue::try_from(cookie.as_str()) { + ctx.add_response_header(SET_COOKIE, value); + } + } + Ok(AuthResponse { success: true, message: "登录成功".to_string(), @@ -180,15 +209,40 @@ pub async fn login( #[server(Logout, "/api")] pub async fn logout() -> Result { + let token = if let Some(ctx) = dioxus::fullstack::FullstackContext::current() { + let parts = ctx.parts_mut(); + parts + .headers + .get("cookie") + .and_then(|h| h.to_str().ok()) + .and_then(parse_session_token) + .map(|s| s.to_string()) + } else { + None + }; + let client = DB_POOL .get() .await .map_err(|e| ServerFnError::new(format!("数据库连接失败: {}", e)))?; - client - .execute("DELETE FROM sessions WHERE expires_at < NOW()", &[]) - .await - .map_err(|e| ServerFnError::new(format!("清理 session 失败: {}", e)))?; + // 清除 cookie + if let Some(ctx) = dioxus::fullstack::FullstackContext::current() { + ctx.add_response_header( + SET_COOKIE, + HeaderValue::from_static( + "session=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax", + ), + ); + } + + // 删除当前 session + if let Some(t) = token { + client + .execute("DELETE FROM sessions WHERE token = $1", &[&t]) + .await + .map_err(|e| ServerFnError::new(format!("删除 session 失败: {}", e)))?; + } Ok(AuthResponse { success: true, @@ -204,6 +258,22 @@ pub struct CurrentUserResponse { #[server(GetCurrentUser, "/api")] pub async fn get_current_user() -> Result { + let token = if let Some(ctx) = dioxus::fullstack::FullstackContext::current() { + let parts = ctx.parts_mut(); + parts + .headers + .get("cookie") + .and_then(|h| h.to_str().ok()) + .and_then(parse_session_token) + .map(|s| s.to_string()) + } else { + None + }; + + let Some(token) = token else { + return Ok(CurrentUserResponse { user: None }); + }; + let client = DB_POOL .get() .await @@ -214,10 +284,8 @@ pub async fn get_current_user() -> Result { "SELECT u.id, u.username, u.email, u.password_hash, u.role, u.created_at FROM sessions s JOIN users u ON s.user_id = u.id - WHERE s.expires_at > NOW() - ORDER BY s.created_at DESC - LIMIT 1", - &[], + WHERE s.token = $1 AND s.expires_at > NOW()", + &[&token], ) .await .map_err(|e| ServerFnError::new(format!("查询失败: {}", e)))?; diff --git a/src/components/admin_layout.rs b/src/components/admin_layout.rs index 2819c91..e88f14d 100644 --- a/src/components/admin_layout.rs +++ b/src/components/admin_layout.rs @@ -1,8 +1,5 @@ use dioxus::prelude::*; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::JsCast; - use crate::api::auth::{get_current_user, logout}; use crate::components::header::{Header, NavItemConfig}; use crate::components::footer::Footer; @@ -42,16 +39,6 @@ pub fn AdminLayout(children: Element) -> Element { let nav = nav; spawn(async move { let _ = logout().await; - #[cfg(target_arch = "wasm32")] - { - let cookie = "session=; path=/; max-age=0"; - if let Some(window) = web_sys::window() { - if let Some(document) = window.document() { - let _ = document.dyn_into::() - .map(|d| d.set_cookie(cookie)); - } - } - } let _ = nav.push("/login"); }); }, diff --git a/src/pages/login.rs b/src/pages/login.rs index a494f8e..7e7c113 100644 --- a/src/pages/login.rs +++ b/src/pages/login.rs @@ -1,8 +1,5 @@ use dioxus::prelude::*; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::JsCast; - use crate::api::auth::{login, AuthResponse}; #[component] @@ -24,21 +21,6 @@ pub fn LoginPage() -> Element { token: Some(_token), .. }) => { - #[cfg(target_arch = "wasm32")] - { - let cookie = format!( - "session={}; path=/; max-age={}; SameSite=Lax", - _token, - 30 * 24 * 60 * 60 - ); - if let Some(window) = web_sys::window() { - if let Some(document) = window.document() { - let _ = document - .dyn_into::() - .map(|d| d.set_cookie(&cookie)); - } - } - } let _ = dioxus::router::navigator().push("/admin"); } Ok(AuthResponse {