From 2519ad8feec76489c2cd18d3e7ed2a3dda46cc17 Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 27 Apr 2026 13:05:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E5=BE=AE=E4=BF=A1=20session=5Fke?= =?UTF-8?q?y=20=E8=BF=81=E7=A7=BB=E5=88=B0=20Redis=20=E2=80=94=20=E5=86=85?= =?UTF-8?q?=E5=AD=98=E9=99=8D=E7=BA=A7=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit session_key 从全局 HashMap 迁移到 Redis(SET key EX 300 / GETDEL), Redis 不可用时自动降级到内存缓存,提升多实例部署安全性。 --- Cargo.lock | 1 + crates/erp-auth/Cargo.toml | 1 + crates/erp-auth/src/auth_state.rs | 1 + crates/erp-auth/src/service/wechat_service.rs | 117 ++++++++++++------ crates/erp-server/src/state.rs | 1 + 5 files changed, 85 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b17210f..132116c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1405,6 +1405,7 @@ dependencies = [ "erp-core", "hex", "jsonwebtoken", + "redis", "reqwest", "sea-orm", "serde", diff --git a/crates/erp-auth/Cargo.toml b/crates/erp-auth/Cargo.toml index a975441..fd90484 100644 --- a/crates/erp-auth/Cargo.toml +++ b/crates/erp-auth/Cargo.toml @@ -25,3 +25,4 @@ aes.workspace = true cbc.workspace = true hex.workspace = true base64 = "0.22" +redis.workspace = true diff --git a/crates/erp-auth/src/auth_state.rs b/crates/erp-auth/src/auth_state.rs index d407640..0651433 100644 --- a/crates/erp-auth/src/auth_state.rs +++ b/crates/erp-auth/src/auth_state.rs @@ -23,6 +23,7 @@ pub struct AuthState { pub default_tenant_id: Uuid, pub wechat_appid: String, pub wechat_secret: String, + pub redis: Option, } /// Parse a human-readable TTL string (e.g. "15m", "7d", "1h", "900s") into seconds. diff --git a/crates/erp-auth/src/service/wechat_service.rs b/crates/erp-auth/src/service/wechat_service.rs index 777cea1..47893d2 100644 --- a/crates/erp-auth/src/service/wechat_service.rs +++ b/crates/erp-auth/src/service/wechat_service.rs @@ -8,7 +8,7 @@ use sea_orm::{ use serde::Deserialize; use std::collections::HashMap; use std::sync::LazyLock; -use std::time::{Duration, Instant}; +use std::time::Instant; use tokio::sync::Mutex; use uuid::Uuid; @@ -22,18 +22,19 @@ use erp_core::sanitize::sanitize_string; type Aes128CbcDec = Decryptor; -/// session_key 缓存条目 +/// 内存降级缓存(Redis 不可用时使用) struct SessionEntry { session_key: String, created_at: Instant, } -/// 全局 session_key 缓存(openid → session_key) -/// TTL: 5 分钟(微信 session_key 有效期约 5 分钟) -static SESSION_CACHE: LazyLock>> = +static MEMORY_FALLBACK: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); -const SESSION_TTL: Duration = Duration::from_secs(300); +const SESSION_TTL_SECS: u64 = 300; + +/// Redis key 前缀 +const REDIS_KEY_PREFIX: &str = "wechat:session:"; #[derive(Debug, Deserialize)] struct WechatSessionResp { @@ -65,16 +66,19 @@ impl WechatService { .clone() .ok_or_else(|| AuthError::Validation("微信登录失败:未获取到 openid".to_string()))?; - // 缓存 session_key 供后续 bind_phone 使用 - if let Some(sk) = session.session_key { - let mut cache = SESSION_CACHE.lock().await; - cache.insert( - openid.clone(), - SessionEntry { - session_key: sk, - created_at: Instant::now(), - }, - ); + // 缓存 session_key(Redis 优先,内存降级) + if let Some(sk) = &session.session_key { + if let Err(e) = Self::store_session_key_redis(&state.redis, &openid, sk).await { + tracing::warn!(openid = %openid, error = %e, "Redis session_key 存储失败,降级内存"); + let mut cache = MEMORY_FALLBACK.lock().await; + cache.insert( + openid.clone(), + SessionEntry { + session_key: sk.clone(), + created_at: Instant::now(), + }, + ); + } } let existing = wechat_user::Entity::find() @@ -118,26 +122,8 @@ impl WechatService { encrypted_data: &str, iv: &str, ) -> AuthResult { - // 从缓存获取 session_key - let session_key = { - let mut cache = SESSION_CACHE.lock().await; - if let Some(entry) = cache.get(openid) { - if entry.created_at.elapsed() < SESSION_TTL { - let sk = entry.session_key.clone(); - cache.remove(openid); - sk - } else { - cache.remove(openid); - return Err(AuthError::Validation( - "session_key 已过期,请重新登录".to_string(), - )); - } - } else { - return Err(AuthError::Validation( - "未找到 session_key,请重新登录".to_string(), - )); - } - }; + // 从 Redis 或内存获取 session_key + let session_key = Self::get_session_key(&state.redis, openid).await?; let phone = decrypt_phone_number(&session_key, encrypted_data, iv)?; @@ -235,6 +221,65 @@ impl WechatService { Ok(user_id) } + + async fn store_session_key_redis( + redis: &Option, + openid: &str, + session_key: &str, + ) -> AuthResult<()> { + let client = redis + .as_ref() + .ok_or_else(|| AuthError::DbError("Redis 未配置".into()))?; + let mut conn = client + .get_multiplexed_async_connection() + .await + .map_err(|e| AuthError::DbError(format!("Redis 连接失败: {e}")))?; + let key = format!("{}{}", REDIS_KEY_PREFIX, openid); + redis::cmd("SET") + .arg(&key) + .arg(session_key) + .arg("EX") + .arg(SESSION_TTL_SECS) + .query_async::(&mut conn) + .await + .map_err(|e| AuthError::DbError(format!("Redis SET 失败: {e}")))?; + Ok(()) + } + + async fn get_session_key( + redis: &Option, + openid: &str, + ) -> AuthResult { + // 1. 尝试 Redis + if let Some(client) = redis { + if let Ok(mut conn) = client.get_multiplexed_async_connection().await { + let key = format!("{}{}", REDIS_KEY_PREFIX, openid); + let result: Option = redis::cmd("GETDEL") + .arg(&key) + .query_async::>(&mut conn) + .await + .unwrap_or(None); + if let Some(sk) = result { + return Ok(sk); + } + } + } + + // 2. 降级到内存 + let mut cache = MEMORY_FALLBACK.lock().await; + if let Some(entry) = cache.get(openid) { + if entry.created_at.elapsed().as_secs() < SESSION_TTL_SECS { + let sk = entry.session_key.clone(); + cache.remove(openid); + return Ok(sk); + } + cache.remove(openid); + } + + Err(AuthError::Validation( + "未找到 session_key,请重新登录".to_string(), + )) + } } /// AES-128-CBC 解密微信手机号 diff --git a/crates/erp-server/src/state.rs b/crates/erp-server/src/state.rs index 9aa10ab..51759eb 100644 --- a/crates/erp-server/src/state.rs +++ b/crates/erp-server/src/state.rs @@ -57,6 +57,7 @@ impl FromRef for erp_auth::AuthState { default_tenant_id: state.default_tenant_id, wechat_appid: state.config.wechat.appid.clone(), wechat_secret: state.config.wechat.secret.clone(), + redis: Some(state.redis.clone()), } } }