use std::sync::{Arc, RwLock};
use tokio::sync::oneshot;
use crate::error::TigerError;
use super::config_parser;
const DEFAULT_TOKEN_FILE: &str = "tiger_openapi_token.properties";
const DEFAULT_CHECK_INTERVAL_SECS: u64 = 300;
pub struct TokenManager {
token: Arc<RwLock<String>>,
file_path: String,
file_enabled: bool,
refresh_duration: i64,
check_interval_secs: u64,
token_writer: Option<Arc<dyn Fn(String) + Send + Sync>>,
token_loader: Option<Arc<dyn Fn() -> Result<String, TigerError> + Send + Sync>>,
stop_tx: Arc<RwLock<Option<oneshot::Sender<()>>>>,
}
impl TokenManager {
pub fn new() -> Self {
Self::with_options(None, false, 0, DEFAULT_CHECK_INTERVAL_SECS, None, None)
}
pub fn with_file_path(file_path: &str) -> Self {
Self::with_options(
Some(file_path),
true,
0,
DEFAULT_CHECK_INTERVAL_SECS,
None,
None,
)
}
pub fn with_refresh_duration(file_path: Option<&str>, refresh_duration_secs: i64) -> Self {
let dur = if refresh_duration_secs > 0 && refresh_duration_secs < 30 {
30
} else {
refresh_duration_secs
};
Self::with_options(
file_path,
file_path.is_some(),
dur,
DEFAULT_CHECK_INTERVAL_SECS,
None,
None,
)
}
fn with_options(
file_path: Option<&str>,
file_enabled: bool,
refresh_duration: i64,
check_interval_secs: u64,
token_writer: Option<Arc<dyn Fn(String) + Send + Sync>>,
token_loader: Option<Arc<dyn Fn() -> Result<String, TigerError> + Send + Sync>>,
) -> Self {
Self {
token: Arc::new(RwLock::new(String::new())),
file_path: file_path.unwrap_or(DEFAULT_TOKEN_FILE).to_string(),
file_enabled,
refresh_duration,
check_interval_secs,
token_writer,
token_loader,
stop_tx: Arc::new(RwLock::new(None)),
}
}
pub fn set_refresh_duration(&mut self, secs: i64) {
self.refresh_duration = if secs > 0 && secs < 30 { 30 } else { secs };
}
pub fn set_check_interval(&mut self, secs: u64) {
self.check_interval_secs = if secs == 0 { DEFAULT_CHECK_INTERVAL_SECS } else { secs };
}
pub fn set_token_writer<F>(&mut self, writer: F)
where
F: Fn(String) + Send + Sync + 'static,
{
self.token_writer = Some(Arc::new(writer));
}
pub fn set_token_loader<F>(&mut self, loader: F)
where
F: Fn() -> Result<String, TigerError> + Send + Sync + 'static,
{
self.token_loader = Some(Arc::new(loader));
}
pub fn load_token(&self) -> Result<String, TigerError> {
if let Some(ref loader) = self.token_loader {
let token = loader().map_err(|e| {
TigerError::Config(format!("自定义 token 加载失败: {}", e))
})?;
if token.is_empty() {
return Err(TigerError::Config("自定义 token 加载返回空值".to_string()));
}
*self.token.write().unwrap() = token.clone();
return Ok(token);
}
let props = config_parser::parse_properties_file(&self.file_path)?;
let token = props.get("token").cloned().unwrap_or_default();
if token.is_empty() {
return Err(TigerError::Config(
"Token 文件中未找到 token 字段".to_string(),
));
}
*self.token.write().unwrap() = token.clone();
Ok(token)
}
pub fn get_token(&self) -> String {
self.token.read().unwrap().clone()
}
pub fn set_token(&self, token: &str) -> Result<(), TigerError> {
*self.token.write().unwrap() = token.to_string();
if self.file_enabled {
self.save_token_to_file(token)?;
}
if let Some(ref writer) = self.token_writer {
writer(token.to_string());
}
Ok(())
}
pub fn sync_token(&self, token: &str) {
*self.token.write().unwrap() = token.to_string();
}
pub fn should_token_refresh(&self) -> bool {
let token = self.token.read().unwrap().clone();
if token.is_empty() || self.refresh_duration == 0 {
return false;
}
use base64::Engine;
let decoded = match base64::engine::general_purpose::STANDARD.decode(&token) {
Ok(d) => d,
Err(_) => return false,
};
if decoded.len() < 27 {
return false;
}
let header = match std::str::from_utf8(&decoded[..27]) {
Ok(s) => s,
Err(_) => return false,
};
let parts: Vec<&str> = header.splitn(2, ',').collect();
if parts.len() < 2 {
return false;
}
let gen_ts: i64 = match parts[0].trim().parse() {
Ok(v) => v,
Err(_) => return false,
};
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
(now_secs - gen_ts / 1000) > self.refresh_duration
}
pub fn start_auto_refresh<F, Fut>(&self, refresh_fn: F)
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<String, TigerError>> + Send + 'static,
{
self.stop_auto_refresh();
let (tx, rx) = oneshot::channel::<()>();
*self.stop_tx.write().unwrap() = Some(tx);
let token = Arc::clone(&self.token);
let file_path = self.file_path.clone();
let file_enabled = self.file_enabled;
let refresh_duration = self.refresh_duration;
let check_interval = std::time::Duration::from_secs(self.check_interval_secs);
let token_writer = self.token_writer.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(check_interval);
let mut rx = rx;
loop {
tokio::select! {
_ = &mut rx => {
return;
}
_ = interval.tick() => {
let needs_refresh = {
let t = token.read().unwrap().clone();
if t.is_empty() || refresh_duration == 0 {
false
} else {
should_refresh_token(&t, refresh_duration)
}
};
if !needs_refresh {
continue;
}
match refresh_fn().await {
Ok(new_token) if !new_token.is_empty() => {
*token.write().unwrap() = new_token.clone();
if file_enabled {
let _ = write_token_to_file(&file_path, &new_token);
}
if let Some(ref writer) = token_writer {
writer(new_token);
}
}
_ => {}
}
}
}
}
});
}
pub fn stop_auto_refresh(&self) {
let tx = self.stop_tx.write().unwrap().take();
if let Some(tx) = tx {
let _ = tx.send(());
}
}
fn save_token_to_file(&self, token: &str) -> Result<(), TigerError> {
write_token_to_file(&self.file_path, token)
}
}
impl Default for TokenManager {
fn default() -> Self {
Self::new()
}
}
impl Drop for TokenManager {
fn drop(&mut self) {
self.stop_auto_refresh();
}
}
fn should_refresh_token(token: &str, refresh_duration: i64) -> bool {
use base64::Engine;
let decoded = match base64::engine::general_purpose::STANDARD.decode(token) {
Ok(d) => d,
Err(_) => return false,
};
if decoded.len() < 27 {
return false;
}
let header = match std::str::from_utf8(&decoded[..27]) {
Ok(s) => s,
Err(_) => return false,
};
let parts: Vec<&str> = header.splitn(2, ',').collect();
if parts.len() < 2 {
return false;
}
let gen_ts: i64 = match parts[0].trim().parse() {
Ok(v) => v,
Err(_) => return false,
};
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
(now_secs - gen_ts / 1000) > refresh_duration
}
fn write_token_to_file(file_path: &str, token: &str) -> Result<(), TigerError> {
let path = std::path::Path::new(file_path);
if let Some(dir) = path.parent() {
if !dir.as_os_str().is_empty() {
std::fs::create_dir_all(dir).map_err(|e| {
TigerError::Config(format!("创建目录失败: {}", e))
})?;
}
}
let content = format!("token={}\n", token);
std::fs::write(file_path, content).map_err(|e| {
TigerError::Config(format!("写入 Token 文件失败: {}", e))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_token() {
let dir = std::env::temp_dir();
let path = dir.join("test_rust_token.properties");
std::fs::write(&path, "token=test_token_123\n").unwrap();
let m = TokenManager::with_file_path(path.to_str().unwrap());
let token = m.load_token().unwrap();
assert_eq!(token, "test_token_123");
assert_eq!(m.get_token(), "test_token_123");
std::fs::remove_file(&path).ok();
}
#[test]
fn test_load_token_file_not_found() {
let m = TokenManager::with_file_path("/nonexistent/path");
assert!(m.load_token().is_err());
}
#[test]
fn test_load_token_no_field() {
let dir = std::env::temp_dir();
let path = dir.join("test_rust_token_nofield.properties");
std::fs::write(&path, "other_key=value\n").unwrap();
let m = TokenManager::with_file_path(path.to_str().unwrap());
assert!(m.load_token().is_err());
std::fs::remove_file(&path).ok();
}
#[test]
fn test_load_token_via_custom_loader() {
let m = TokenManager::new();
let mut m = m;
m.set_token_loader(|| Ok("loader_token".to_string()));
let token = m.load_token().unwrap();
assert_eq!(token, "loader_token");
assert_eq!(m.get_token(), "loader_token");
}
#[test]
fn test_set_token_writes_file() {
let dir = std::env::temp_dir();
let path = dir.join("test_rust_token_set.properties");
let m = TokenManager::with_file_path(path.to_str().unwrap());
m.set_token("new_token_456").unwrap();
assert_eq!(m.get_token(), "new_token_456");
let content = std::fs::read_to_string(&path).unwrap();
assert_eq!(content, "token=new_token_456\n");
std::fs::remove_file(&path).ok();
}
#[test]
fn test_set_token_no_file_when_file_disabled() {
let m = TokenManager::new(); m.set_token("mem_only_token").unwrap();
assert_eq!(m.get_token(), "mem_only_token");
}
#[test]
fn test_set_token_triggers_writer() {
use std::sync::{Arc, Mutex};
let captured: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
let captured_clone = Arc::clone(&captured);
let mut m = TokenManager::new();
m.set_token_writer(move |t| {
*captured_clone.lock().unwrap() = t;
});
m.set_token("callback_token").unwrap();
assert_eq!(*captured.lock().unwrap(), "callback_token");
}
#[test]
fn test_sync_token_no_file_no_callback() {
use std::sync::{Arc, Mutex};
let called: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
let called_clone = Arc::clone(&called);
let dir = std::env::temp_dir();
let path = dir.join("test_rust_sync_token.properties");
let mut m = TokenManager::with_file_path(path.to_str().unwrap());
m.set_token_writer(move |_| {
*called_clone.lock().unwrap() = true;
});
m.sync_token("sync_only");
assert_eq!(m.get_token(), "sync_only");
assert!(!path.exists());
assert!(!*called.lock().unwrap());
}
#[test]
fn test_set_then_load() {
let dir = std::env::temp_dir();
let path = dir.join("test_rust_token_roundtrip.properties");
let m1 = TokenManager::with_file_path(path.to_str().unwrap());
m1.set_token("round_trip_token").unwrap();
let m2 = TokenManager::with_file_path(path.to_str().unwrap());
let token = m2.load_token().unwrap();
assert_eq!(token, "round_trip_token");
std::fs::remove_file(&path).ok();
}
fn make_test_token(gen_ts_ms: i64, expire_ts_ms: i64) -> String {
use base64::Engine;
let header = format!("{:013},{:013}", gen_ts_ms, expire_ts_ms);
let payload = format!("{}some_extra_payload_data", header);
base64::engine::general_purpose::STANDARD.encode(payload.as_bytes())
}
#[test]
fn test_should_token_refresh_empty_token() {
let m = TokenManager::with_refresh_duration(None, 30);
assert!(!m.should_token_refresh());
}
#[test]
fn test_should_token_refresh_zero_duration() {
let m = TokenManager::new();
m.set_token("some_token").ok();
assert!(!m.should_token_refresh());
}
#[test]
fn test_should_token_refresh_expired() {
let m = TokenManager::with_refresh_duration(None, 30);
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let old_gen_ts = (now_secs - 100) * 1000;
let token = make_test_token(old_gen_ts, old_gen_ts + 3600000);
*m.token.write().unwrap() = token;
assert!(m.should_token_refresh());
}
#[test]
fn test_should_token_refresh_not_expired() {
let m = TokenManager::with_refresh_duration(None, 3600);
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let fresh_gen_ts = now_secs * 1000;
let token = make_test_token(fresh_gen_ts, fresh_gen_ts + 7200000);
*m.token.write().unwrap() = token;
assert!(!m.should_token_refresh());
}
#[test]
fn test_should_token_refresh_invalid_token() {
let m = TokenManager::with_refresh_duration(None, 30);
*m.token.write().unwrap() = "not_valid_base64!!!".to_string();
assert!(!m.should_token_refresh());
}
#[test]
fn test_with_refresh_duration_min_30() {
let m = TokenManager::with_refresh_duration(None, 10);
assert_eq!(m.refresh_duration, 30);
}
#[tokio::test]
async fn test_start_auto_refresh_triggers_refresh() {
use std::sync::{Arc, Mutex};
let call_count: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
let call_count_clone = Arc::clone(&call_count);
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let old_gen_ts = (now_secs - 100) * 1000;
let expired_token = make_test_token(old_gen_ts, old_gen_ts + 3600000);
let mut m = TokenManager::with_refresh_duration(None, 30);
m.set_check_interval(0); m.check_interval_secs = 0; m.check_interval_secs = 1; m.set_token(&expired_token).unwrap();
let updated_token: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
let updated_token_clone = Arc::clone(&updated_token);
m.set_token_writer(move |t| {
*updated_token_clone.lock().unwrap() = t;
});
m.start_auto_refresh(move || {
let count = Arc::clone(&call_count_clone);
async move {
*count.lock().unwrap() += 1;
Ok("refreshed_token".to_string())
}
});
tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
m.stop_auto_refresh();
assert!(*call_count.lock().unwrap() > 0, "refresh should have been called");
assert_eq!(m.get_token(), "refreshed_token");
assert_eq!(*updated_token.lock().unwrap(), "refreshed_token");
}
#[tokio::test]
async fn test_stop_auto_refresh_stops_background_task() {
use std::sync::{Arc, Mutex};
let call_count: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
let call_count_clone = Arc::clone(&call_count);
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let old_gen_ts = (now_secs - 100) * 1000;
let expired_token = make_test_token(old_gen_ts, old_gen_ts + 3600000);
let mut m = TokenManager::with_refresh_duration(None, 30);
m.check_interval_secs = 1;
m.set_token(&expired_token).unwrap();
m.start_auto_refresh(move || {
let count = Arc::clone(&call_count_clone);
async move {
*count.lock().unwrap() += 1;
Ok("new_token".to_string())
}
});
tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
m.stop_auto_refresh();
let count_after_stop = *call_count.lock().unwrap();
tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
let count_final = *call_count.lock().unwrap();
assert_eq!(count_after_stop, count_final, "no more refreshes after stop");
}
}