//! 会话 token 生成、哈希与 Cookie 处理。 //! //! token 使用 UUID,存储时使用 SHA-256 哈希, //! Cookie 包含 HttpOnly、SameSite=Lax 与可选 Secure 标志。 //! 服务端上下文解析函数仅在 `feature = "server"` 时可用。 #[cfg(feature = "server")] use chrono::{DateTime, Duration, Utc}; #[cfg(feature = "server")] use sha2::{Digest, Sha256}; #[cfg(feature = "server")] use uuid::Uuid; #[cfg(feature = "server")] /// 生成新的随机会话 token(UUID 格式)。 pub fn generate_token() -> String { Uuid::new_v4().to_string() } #[cfg(feature = "server")] /// 使用 SHA-256 对 token 进行哈希,用于数据库存储。 pub fn hash_token(token: &str) -> String { let mut hasher = Sha256::new(); hasher.update(token.as_bytes()); hex::encode(hasher.finalize()) } #[cfg(feature = "server")] /// 返回默认会话过期时间(当前时间 + 30 天)。 pub fn default_expiry() -> DateTime { Utc::now() + Duration::days(30) } #[cfg(feature = "server")] /// 读取环境变量 `COOKIE_SECURE`,决定是否给 Cookie 添加 Secure 标志。 pub fn cookie_secure() -> bool { std::env::var("COOKIE_SECURE") .ok() .map(|v| matches!(v.as_str(), "1" | "true" | "yes")) .unwrap_or(false) } #[cfg(feature = "server")] /// 构造会话 Cookie 字符串,包含 HttpOnly、Path、Max-Age 与 SameSite。 pub fn session_cookie(token: &str, max_age_seconds: i32, secure: bool) -> String { let secure_flag = if secure { "; Secure" } else { "" }; format!( "session={token}; HttpOnly; Path=/; Max-Age={max_age_seconds}; SameSite=Lax{secure_flag}" ) } #[cfg(feature = "server")] /// 从 `Cookie` 头中解析名为 `session` 的 token 值。 pub 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 } }) } #[cfg(feature = "server")] /// 从 Dioxus `FullstackContext` 中读取 Cookie 并返回会话 token。 pub fn get_session_from_ctx() -> Option { use dioxus::fullstack::FullstackContext; FullstackContext::current().and_then(|ctx| { 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()) }) } #[cfg(all(test, feature = "server"))] mod tests { use super::*; #[test] fn parse_session_found() { let header = "session=abc123; path=/"; assert_eq!(parse_session_token(header), Some("abc123")); } #[test] fn parse_session_single_cookie() { assert_eq!(parse_session_token("session=token456"), Some("token456")); } #[test] fn parse_session_not_found() { assert_eq!(parse_session_token("other=value"), None); } #[test] fn parse_session_empty_string() { assert_eq!(parse_session_token(""), None); } #[test] fn parse_session_multiple_cookies() { let header = "theme=dark; session=my-secret; lang=en"; assert_eq!(parse_session_token(header), Some("my-secret")); } #[test] fn parse_session_empty_value() { assert_eq!(parse_session_token("session="), Some("")); } #[test] fn parse_session_trailing_semicolon() { assert_eq!(parse_session_token("session=abc;"), Some("abc")); } #[test] fn generate_token_is_uuid() { let token = generate_token(); assert!(uuid::Uuid::parse_str(&token).is_ok()); } #[test] fn default_expiry_is_future() { let expiry = default_expiry(); assert!(expiry > chrono::Utc::now()); } #[test] fn default_expiry_is_about_30_days() { let expiry = default_expiry(); let diff = expiry - chrono::Utc::now(); assert!(diff.num_days() >= 29 && diff.num_days() <= 31); } #[test] fn hash_token_is_deterministic() { let token = "test-token-123"; assert_eq!(hash_token(token), hash_token(token)); } #[test] fn hash_token_is_64_chars() { let hash = hash_token("any-token"); assert_eq!(hash.len(), 64); } #[test] fn hash_token_differs_from_input() { let token = "my-secret-token"; assert_ne!(hash_token(token), token); } #[test] fn hash_token_known_value() { let hash = hash_token("hello"); let expected = sha2::Sha256::digest(b"hello"); assert_eq!(hash, hex::encode(expected)); } #[test] fn session_cookie_without_secure() { let cookie = session_cookie("abc", 3600, false); assert!(cookie.contains("session=abc")); assert!(cookie.contains("HttpOnly")); assert!(cookie.contains("SameSite=Lax")); assert!(!cookie.contains("Secure")); } #[test] fn session_cookie_with_secure() { let cookie = session_cookie("abc", 3600, true); assert!(cookie.contains("Secure")); } #[test] fn session_cookie_logout_has_zero_max_age() { let cookie = session_cookie("", 0, false); assert!(cookie.contains("Max-Age=0")); } }