将 session cookie 改为后端 HttpOnly 设置
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
e358f2af7d
commit
0be0719fdb
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -4629,6 +4629,7 @@ dependencies = [
|
|||||||
"dioxus",
|
"dioxus",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
|
"http",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
"rand 0.8.6",
|
"rand 0.8.6",
|
||||||
|
|||||||
@ -17,6 +17,7 @@ pulldown-cmark = "0.13"
|
|||||||
dotenvy = { version = "0.15", optional = true }
|
dotenvy = { version = "0.15", optional = true }
|
||||||
rand = { version = "0.8", features = ["getrandom"] }
|
rand = { version = "0.8", features = ["getrandom"] }
|
||||||
getrandom = { version = "0.2", features = ["js"] }
|
getrandom = { version = "0.2", features = ["js"] }
|
||||||
|
http = "1"
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
web-sys = { version = "0.3", features = ["Document", "Window", "HtmlDocument", "Storage", "Element", "DomTokenList", "MediaQueryList", "HtmlScriptElement"] }
|
web-sys = { version = "0.3", features = ["Document", "Window", "HtmlDocument", "Storage", "Element", "DomTokenList", "MediaQueryList", "HtmlScriptElement"] }
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
#![allow(clippy::unused_unit, deprecated, unused_imports)]
|
#![allow(clippy::unused_unit, deprecated, unused_imports)]
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
use http::header::{HeaderValue, SET_COOKIE};
|
||||||
|
|
||||||
use crate::auth::{password, session};
|
use crate::auth::{password, session};
|
||||||
use crate::db::pool::DB_POOL;
|
use crate::db::pool::DB_POOL;
|
||||||
@ -34,6 +36,23 @@ fn validate_password(password: &str) -> Result<(), String> {
|
|||||||
Ok(())
|
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)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct AuthResponse {
|
pub struct AuthResponse {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
@ -171,6 +190,16 @@ pub async fn login(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| ServerFnError::new(format!("创建 session 失败: {}", e)))?;
|
.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 {
|
Ok(AuthResponse {
|
||||||
success: true,
|
success: true,
|
||||||
message: "登录成功".to_string(),
|
message: "登录成功".to_string(),
|
||||||
@ -180,15 +209,40 @@ pub async fn login(
|
|||||||
|
|
||||||
#[server(Logout, "/api")]
|
#[server(Logout, "/api")]
|
||||||
pub async fn logout() -> Result<AuthResponse, ServerFnError> {
|
pub async fn logout() -> Result<AuthResponse, ServerFnError> {
|
||||||
|
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
|
let client = DB_POOL
|
||||||
.get()
|
.get()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ServerFnError::new(format!("数据库连接失败: {}", e)))?;
|
.map_err(|e| ServerFnError::new(format!("数据库连接失败: {}", e)))?;
|
||||||
|
|
||||||
client
|
// 清除 cookie
|
||||||
.execute("DELETE FROM sessions WHERE expires_at < NOW()", &[])
|
if let Some(ctx) = dioxus::fullstack::FullstackContext::current() {
|
||||||
.await
|
ctx.add_response_header(
|
||||||
.map_err(|e| ServerFnError::new(format!("清理 session 失败: {}", e)))?;
|
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 {
|
Ok(AuthResponse {
|
||||||
success: true,
|
success: true,
|
||||||
@ -204,6 +258,22 @@ pub struct CurrentUserResponse {
|
|||||||
|
|
||||||
#[server(GetCurrentUser, "/api")]
|
#[server(GetCurrentUser, "/api")]
|
||||||
pub async fn get_current_user() -> Result<CurrentUserResponse, ServerFnError> {
|
pub async fn get_current_user() -> Result<CurrentUserResponse, ServerFnError> {
|
||||||
|
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
|
let client = DB_POOL
|
||||||
.get()
|
.get()
|
||||||
.await
|
.await
|
||||||
@ -214,10 +284,8 @@ pub async fn get_current_user() -> Result<CurrentUserResponse, ServerFnError> {
|
|||||||
"SELECT u.id, u.username, u.email, u.password_hash, u.role, u.created_at
|
"SELECT u.id, u.username, u.email, u.password_hash, u.role, u.created_at
|
||||||
FROM sessions s
|
FROM sessions s
|
||||||
JOIN users u ON s.user_id = u.id
|
JOIN users u ON s.user_id = u.id
|
||||||
WHERE s.expires_at > NOW()
|
WHERE s.token = $1 AND s.expires_at > NOW()",
|
||||||
ORDER BY s.created_at DESC
|
&[&token],
|
||||||
LIMIT 1",
|
|
||||||
&[],
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ServerFnError::new(format!("查询失败: {}", e)))?;
|
.map_err(|e| ServerFnError::new(format!("查询失败: {}", e)))?;
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
|
|
||||||
use crate::api::auth::{get_current_user, logout};
|
use crate::api::auth::{get_current_user, logout};
|
||||||
use crate::components::header::{Header, NavItemConfig};
|
use crate::components::header::{Header, NavItemConfig};
|
||||||
use crate::components::footer::Footer;
|
use crate::components::footer::Footer;
|
||||||
@ -42,16 +39,6 @@ pub fn AdminLayout(children: Element) -> Element {
|
|||||||
let nav = nav;
|
let nav = nav;
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let _ = logout().await;
|
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::<web_sys::HtmlDocument>()
|
|
||||||
.map(|d| d.set_cookie(cookie));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let _ = nav.push("/login");
|
let _ = nav.push("/login");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
|
|
||||||
use crate::api::auth::{login, AuthResponse};
|
use crate::api::auth::{login, AuthResponse};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
@ -24,21 +21,6 @@ pub fn LoginPage() -> Element {
|
|||||||
token: Some(_token),
|
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::<web_sys::HtmlDocument>()
|
|
||||||
.map(|d| d.set_cookie(&cookie));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let _ = dioxus::router::navigator().push("/admin");
|
let _ = dioxus::router::navigator().push("/admin");
|
||||||
}
|
}
|
||||||
Ok(AuthResponse {
|
Ok(AuthResponse {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user