g3proxy: introduce a cache layer for remote user passwords

This commit is contained in:
Zhang Jingqiang 2026-02-04 23:07:51 +08:00
parent 7dcf3974a8
commit 4a97072987
6 changed files with 150 additions and 2 deletions

91
g3proxy/src/auth/cache.rs Normal file
View file

@ -0,0 +1,91 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2026 G3-OSS developers.
*/
use std::cell::RefCell;
use std::collections::{BTreeMap, HashMap};
use std::num::NonZeroUsize;
use std::time::Duration;
use foldhash::fast::FixedState;
use lru::LruCache;
use tokio::time::Instant;
use g3_types::metrics::NodeName;
thread_local! {
static CACHE: RefCell<HashMap<NodeName, GroupLocalCache, FixedState>> = const {
RefCell::new(HashMap::with_hasher(FixedState::with_seed(0)))
};
}
#[derive(Default)]
struct UserLocalCache {
password_map: BTreeMap<String, Instant>,
}
struct GroupLocalCache {
user_map: LruCache<String, UserLocalCache, FixedState>,
}
impl Default for GroupLocalCache {
fn default() -> Self {
GroupLocalCache::new(crate::config::auth::group::DEFAULT_CACHE_USER_COUNT)
}
}
impl GroupLocalCache {
fn new(user_count: NonZeroUsize) -> Self {
GroupLocalCache {
user_map: LruCache::with_hasher(user_count, FixedState::with_seed(0)),
}
}
}
pub(super) fn has_valid_password(group: &NodeName, username: &str, password: &str) -> bool {
CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
let group = cache
.entry(group.clone())
.or_insert_with(GroupLocalCache::default);
let Some(user) = group.user_map.get_mut(username) else {
return false;
};
let Some((p, t)) = user.password_map.remove_entry(password) else {
return false;
};
if t > Instant::now() {
user.password_map.insert(p, t);
true
} else {
false
}
})
}
pub(super) fn save_user_password(
group: &NodeName,
user_count: NonZeroUsize,
username: String,
password: String,
expire_time: Duration,
) {
CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
let group = cache
.entry(group.clone())
.and_modify(|g| g.user_map.resize(user_count))
.or_insert_with(|| GroupLocalCache::new(user_count));
let user = group
.user_map
.get_or_insert_mut(username, UserLocalCache::default);
user.password_map
.insert(password, Instant::now() + expire_time);
})
}

View file

@ -11,7 +11,7 @@ use tokio::sync::{mpsc, oneshot};
use g3_types::auth::UserAuthError;
use crate::config::auth::LdapUserGroupConfig;
use crate::config::auth::{LdapUserGroupConfig, UserGroupConfig};
mod connect;
use connect::LdapConnector;
@ -51,6 +51,14 @@ impl LdapAuthPoolHandle {
username: &str,
password: &str,
) -> Result<(), UserAuthError> {
if crate::auth::cache::has_valid_password(
self.config.basic_config().name(),
username,
password,
) {
return Ok(());
}
let (sender, receiver) = oneshot::channel();
let req = LdapAuthRequest {
username: username.to_string(),
@ -65,7 +73,16 @@ impl LdapAuthPoolHandle {
let _ = self.req_sender.send(req).await;
match tokio::time::timeout(self.config.queue_wait_timeout, receiver).await {
Ok(Ok(Some(_))) => Ok(()),
Ok(Ok(Some((username, password)))) => {
crate::auth::cache::save_user_password(
self.config.basic_config().name(),
self.config.cache_user_count,
username,
password,
self.config.cache_expire_time,
);
Ok(())
}
Ok(Ok(None)) => Err(UserAuthError::TokenNotMatch),
Ok(Err(_)) => Err(UserAuthError::RemoteError),
Err(_) => Err(UserAuthError::RemoteTimeout),

View file

@ -10,6 +10,8 @@ pub(crate) use ops::reload;
mod registry;
pub(crate) use registry::{get_all_groups, get_names, get_or_insert_default};
mod cache;
mod site;
pub(crate) use site::UserSite;
use site::UserSites;

View file

@ -3,6 +3,7 @@
* Copyright 2026 G3-OSS developers.
*/
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::time::Duration;
@ -34,6 +35,8 @@ pub(crate) struct LdapUserGroupConfig {
pub(crate) connection_pool: ConnectionPoolConfig,
pub(crate) queue_channel_size: usize,
pub(crate) queue_wait_timeout: Duration,
pub(crate) cache_user_count: NonZeroUsize,
pub(crate) cache_expire_time: Duration,
}
impl LdapUserGroupConfig {
@ -53,6 +56,8 @@ impl LdapUserGroupConfig {
connection_pool: ConnectionPoolConfig::new(1024, 8),
queue_channel_size: 64,
queue_wait_timeout: Duration::from_secs(4),
cache_user_count: super::DEFAULT_CACHE_USER_COUNT,
cache_expire_time: super::DEFAULT_CACHE_EXPIRE_TIME,
}
}
@ -169,6 +174,15 @@ impl LdapUserGroupConfig {
.context(format!("invalid humanize duration value for key {k}"))?;
Ok(())
}
"cache_user_count" => {
self.cache_user_count = g3_yaml::value::as_nonzero_usize(v)?;
Ok(())
}
"cache_expire_time" => {
self.cache_expire_time = g3_yaml::humanize::as_duration(v)
.context(format!("invalid humanize duration value for key {k}"))?;
Ok(())
}
_ => self.basic.set(k, v),
}
}

View file

@ -3,6 +3,9 @@
* Copyright 2026 G3-OSS developers.
*/
use std::num::NonZeroUsize;
use std::time::Duration;
use g3_macros::AnyConfig;
use g3_types::metrics::NodeName;
use g3_yaml::YamlDocPosition;
@ -16,6 +19,9 @@ pub(crate) use facts::FactsUserGroupConfig;
mod ldap;
pub(crate) use ldap::LdapUserGroupConfig;
pub(crate) const DEFAULT_CACHE_USER_COUNT: NonZeroUsize = NonZeroUsize::new(128).unwrap();
const DEFAULT_CACHE_EXPIRE_TIME: Duration = Duration::from_secs(300);
pub(crate) trait UserGroupConfig {
fn basic_config(&self) -> &BasicUserGroupConfig;

View file

@ -126,3 +126,21 @@ queue_wait_timeout
Set the timeout value when auth with the LDAP server for a client request.
**default**: 4s
cache_user_count
----------------
**optional**, **type**: usize
Set how many users will be LRU cached in thread local storage.
**default**: 128
cache_expire_time
-----------------
**optional**, **type**: :ref:`humanize duration <conf_value_humanize_duration>`
Set the expire time for valid passwords in the thread local LRU cache.
**default**: 5min