feat(auth): 微信 session_key 迁移到 Redis — 内存降级兜底
session_key 从全局 HashMap 迁移到 Redis(SET key EX 300 / GETDEL), Redis 不可用时自动降级到内存缓存,提升多实例部署安全性。
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1405,6 +1405,7 @@ dependencies = [
|
||||
"erp-core",
|
||||
"hex",
|
||||
"jsonwebtoken",
|
||||
"redis",
|
||||
"reqwest",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
|
||||
@@ -25,3 +25,4 @@ aes.workspace = true
|
||||
cbc.workspace = true
|
||||
hex.workspace = true
|
||||
base64 = "0.22"
|
||||
redis.workspace = true
|
||||
|
||||
@@ -23,6 +23,7 @@ pub struct AuthState {
|
||||
pub default_tenant_id: Uuid,
|
||||
pub wechat_appid: String,
|
||||
pub wechat_secret: String,
|
||||
pub redis: Option<redis::Client>,
|
||||
}
|
||||
|
||||
/// Parse a human-readable TTL string (e.g. "15m", "7d", "1h", "900s") into seconds.
|
||||
|
||||
@@ -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<aes::Aes128>;
|
||||
|
||||
/// 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<Mutex<HashMap<String, SessionEntry>>> =
|
||||
static MEMORY_FALLBACK: LazyLock<Mutex<HashMap<String, SessionEntry>>> =
|
||||
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<LoginResp> {
|
||||
// 从缓存获取 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<redis::Client>,
|
||||
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::<String>(&mut conn)
|
||||
.await
|
||||
.map_err(|e| AuthError::DbError(format!("Redis SET 失败: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_session_key(
|
||||
redis: &Option<redis::Client>,
|
||||
openid: &str,
|
||||
) -> AuthResult<String> {
|
||||
// 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<String> = redis::cmd("GETDEL")
|
||||
.arg(&key)
|
||||
.query_async::<Option<String>>(&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 解密微信手机号
|
||||
|
||||
@@ -57,6 +57,7 @@ impl FromRef<AppState> 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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user