feat(auth): add in-memory session cache
This commit is contained in:
parent
75b8f80631
commit
1d216faa2f
@ -293,6 +293,7 @@ pub async fn logout() -> Result<AuthResponse, ServerFnError> {
|
|||||||
|
|
||||||
if let Some(t) = token {
|
if let Some(t) = token {
|
||||||
let token_hash = session::hash_token(&t);
|
let token_hash = session::hash_token(&t);
|
||||||
|
crate::cache::invalidate_session_user(&token_hash).await;
|
||||||
client
|
client
|
||||||
.execute("DELETE FROM sessions WHERE token_hash = $1", &[&token_hash])
|
.execute("DELETE FROM sessions WHERE token_hash = $1", &[&token_hash])
|
||||||
.await
|
.await
|
||||||
@ -316,11 +317,17 @@ pub struct CurrentUserResponse {
|
|||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
/// 根据会话 token 查询对应用户(含密码哈希等完整信息)。
|
/// 根据会话 token 查询对应用户(含密码哈希等完整信息)。
|
||||||
///
|
///
|
||||||
|
/// 优先命中内存缓存,避免每次请求都执行 DB JOIN;未命中时回查数据库并回填缓存。
|
||||||
/// 仅服务端内部使用,不会暴露给前端。
|
/// 仅服务端内部使用,不会暴露给前端。
|
||||||
pub async fn get_user_by_token(token: &str) -> Result<Option<User>, ServerFnError> {
|
pub async fn get_user_by_token(token: &str) -> Result<Option<User>, ServerFnError> {
|
||||||
|
let token_hash = session::hash_token(token);
|
||||||
|
|
||||||
|
if let Some(user) = crate::cache::get_session_user(&token_hash).await {
|
||||||
|
return Ok(Some(user));
|
||||||
|
}
|
||||||
|
|
||||||
let client = get_conn().await.map_err(AppError::db_conn)?;
|
let client = get_conn().await.map_err(AppError::db_conn)?;
|
||||||
|
|
||||||
let token_hash = session::hash_token(token);
|
|
||||||
let row = client
|
let row = client
|
||||||
.query_opt(
|
.query_opt(
|
||||||
"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
|
||||||
@ -348,6 +355,10 @@ pub async fn get_user_by_token(token: &str) -> Result<Option<User>, ServerFnErro
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(ref u) = user {
|
||||||
|
crate::cache::set_session_user(&token_hash, u.clone()).await;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
66
src/cache.rs
66
src/cache.rs
@ -15,6 +15,8 @@ use std::time::Duration;
|
|||||||
use crate::models::comment::PublicComment;
|
use crate::models::comment::PublicComment;
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
use crate::models::post::{Post, PostListItem, PostStats, Tag};
|
use crate::models::post::{Post, PostListItem, PostStats, Tag};
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
use crate::models::user::User;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 缓存 TTL 配置
|
// 缓存 TTL 配置
|
||||||
@ -48,6 +50,10 @@ const TTL_COMMENTS: Duration = Duration::from_secs(60);
|
|||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
const TTL_PENDING_COUNT: Duration = Duration::from_secs(10);
|
const TTL_PENDING_COUNT: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
/// 会话用户缓存 TTL:300 秒(5 分钟),短于 DB 会话过期时间。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
const TTL_SESSION: Duration = Duration::from_secs(300);
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 缓存 Key 类型
|
// 缓存 Key 类型
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -161,6 +167,19 @@ static PENDING_COUNT_CACHE: LazyLock<Cache<CacheKey, i64>> = LazyLock::new(|| {
|
|||||||
.build()
|
.build()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// 会话用户缓存类型。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
pub type SessionCache = Cache<String, User>;
|
||||||
|
|
||||||
|
/// 全局会话用户缓存实例,最大容量 1000,TTL 5 分钟。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
static SESSION_CACHE: LazyLock<SessionCache> = LazyLock::new(|| {
|
||||||
|
Cache::builder()
|
||||||
|
.max_capacity(1000)
|
||||||
|
.time_to_live(TTL_SESSION)
|
||||||
|
.build()
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 公共缓存 API
|
// 公共缓存 API
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -345,6 +364,24 @@ pub async fn set_pending_count(count: i64) {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 读取会话用户缓存。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
pub async fn get_session_user(token_hash: &str) -> Option<User> {
|
||||||
|
SESSION_CACHE.get(token_hash).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 写入会话用户缓存。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
pub async fn set_session_user(token_hash: &str, user: User) {
|
||||||
|
let _ = SESSION_CACHE.insert(token_hash.to_string(), user).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 失效指定会话用户缓存。
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
pub async fn invalidate_session_user(token_hash: &str) {
|
||||||
|
SESSION_CACHE.invalidate(token_hash).await;
|
||||||
|
}
|
||||||
|
|
||||||
/// 按文章主键失效评论列表缓存。
|
/// 按文章主键失效评论列表缓存。
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub async fn invalidate_comments_by_post(post_id: i32) {
|
pub async fn invalidate_comments_by_post(post_id: i32) {
|
||||||
@ -366,6 +403,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::models::comment::PublicComment;
|
use crate::models::comment::PublicComment;
|
||||||
use crate::models::post::PostStatus;
|
use crate::models::post::PostStatus;
|
||||||
|
use crate::models::user::{User, UserRole};
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -572,4 +610,32 @@ mod tests {
|
|||||||
assert!(get_pending_count().await.is_none());
|
assert!(get_pending_count().await.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn session_cache_roundtrip() {
|
||||||
|
let user = User {
|
||||||
|
id: 42,
|
||||||
|
username: "cached_user".to_string(),
|
||||||
|
email: "cached@example.com".to_string(),
|
||||||
|
password_hash: "argon2_hash".to_string(),
|
||||||
|
role: UserRole::Admin,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
let token_hash = "sha256_token_hash";
|
||||||
|
|
||||||
|
set_session_user(token_hash, user.clone()).await;
|
||||||
|
let cached = get_session_user(token_hash).await;
|
||||||
|
|
||||||
|
assert!(cached.is_some());
|
||||||
|
let cached_user = cached.unwrap();
|
||||||
|
assert_eq!(cached_user.id, user.id);
|
||||||
|
assert_eq!(cached_user.username, user.username);
|
||||||
|
assert_eq!(cached_user.email, user.email);
|
||||||
|
assert_eq!(cached_user.password_hash, user.password_hash);
|
||||||
|
assert_eq!(cached_user.role, user.role);
|
||||||
|
|
||||||
|
invalidate_session_user(token_hash).await;
|
||||||
|
assert!(get_session_user(token_hash).await.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user