将 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",
|
||||
"dotenvy",
|
||||
"getrandom 0.2.17",
|
||||
"http",
|
||||
"js-sys",
|
||||
"pulldown-cmark",
|
||||
"rand 0.8.6",
|
||||
|
||||
@ -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"] }
|
||||
|
||||
@ -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<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
|
||||
.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<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
|
||||
.get()
|
||||
.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
|
||||
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)))?;
|
||||
|
||||
@ -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::<web_sys::HtmlDocument>()
|
||||
.map(|d| d.set_cookie(cookie));
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = nav.push("/login");
|
||||
});
|
||||
},
|
||||
|
||||
@ -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::<web_sys::HtmlDocument>()
|
||||
.map(|d| d.set_cookie(&cookie));
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = dioxus::router::navigator().push("/admin");
|
||||
}
|
||||
Ok(AuthResponse {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user