use crate::config::SaTokenConfig;
use crate::dao::SaTokenDao;
use crate::error::{SaTokenError, SaTokenResult};
use chrono::{DateTime, Utc};
use sa_token_adapter::storage::SaStorage;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NonceRecord {
pub login_id: String,
pub created_at: String,
}
impl NonceRecord {
pub fn new(login_id: impl Into<String>) -> Self {
Self {
login_id: login_id.into(),
created_at: Utc::now().to_rfc3339(),
}
}
}
#[derive(Clone)]
pub struct NonceManager {
dao: Arc<SaTokenDao>,
timeout: i64,
}
impl std::fmt::Debug for NonceManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("NonceManager { .. }")
}
}
impl NonceManager {
pub fn from_dao(dao: Arc<SaTokenDao>, timeout: i64) -> Self {
Self { dao, timeout }
}
pub fn new(storage: Arc<dyn SaStorage>, timeout: i64) -> Self {
let cfg = SaTokenConfig {
nonce_timeout: timeout,
..SaTokenConfig::default()
};
Self::from_dao(Arc::new(SaTokenDao::new(storage, Arc::new(cfg))), timeout)
}
pub fn with_serializer(
mut self,
serializer: sa_token_adapter::serializer::SharedSerializer,
) -> Self {
let mut cfg = (*self.dao.config()).as_ref().clone();
cfg.serializer = serializer;
self.dao = Arc::new(SaTokenDao::new(self.dao.storage().clone(), Arc::new(cfg)));
self
}
fn ttl(&self) -> Option<std::time::Duration> {
if self.timeout > 0 {
Some(std::time::Duration::from_secs(self.timeout as u64))
} else {
None
}
}
pub fn generate(&self) -> String {
format!(
"nonce_{}_{}",
Utc::now().timestamp_millis(),
Uuid::new_v4().simple()
)
}
pub async fn store(&self, nonce: &str, login_id: &str) -> SaTokenResult<()> {
let key = self.dao.keys().nonce(nonce);
let record = NonceRecord::new(login_id);
self.dao.set_object(&key, &record, self.ttl()).await
}
pub async fn get_record(&self, nonce: &str) -> SaTokenResult<Option<NonceRecord>> {
let key = self.dao.keys().nonce(nonce);
self.dao.get_object(&key).await
}
pub async fn validate(&self, nonce: &str) -> SaTokenResult<bool> {
let key = self.dao.keys().nonce(nonce);
Ok(self.dao.get_string(&key).await?.is_none())
}
pub async fn validate_and_consume(&self, nonce: &str, login_id: &str) -> SaTokenResult<()> {
if nonce.trim().is_empty() {
return Err(SaTokenError::InvalidToken("nonce must not be empty".into()));
}
let key = self.dao.keys().nonce(nonce);
let record = NonceRecord::new(login_id);
let raw = self.dao.encode(&record)?;
let occupied = self.dao.set_if_absent(&key, &raw, self.ttl()).await?;
if !occupied {
return Err(SaTokenError::NonceAlreadyUsed);
}
Ok(())
}
pub fn check_timestamp(&self, nonce: &str, window_seconds: i64) -> SaTokenResult<bool> {
let parts: Vec<&str> = nonce.split('_').collect();
if parts.len() < 3 {
return Err(SaTokenError::InvalidNonceFormat);
}
let timestamp_ms: i64 = parts
.get(1)
.ok_or(SaTokenError::InvalidNonceFormat)?
.parse()
.map_err(|_| SaTokenError::InvalidNonceTimestamp)?;
let now_ms = Utc::now().timestamp_millis();
let age_seconds = (now_ms - timestamp_ms) / 1000;
Ok(age_seconds >= 0 && age_seconds <= window_seconds)
}
pub async fn cleanup_expired(&self) -> SaTokenResult<usize> {
if self.timeout <= 0 {
return Ok(0);
}
let pattern = self.dao.keys().scan_pattern("nonce", None);
let mut removed = 0usize;
let mut cursor = 0u64;
let cutoff = Utc::now() - chrono::Duration::seconds(self.timeout);
loop {
let page = match self.dao.scan(&pattern, cursor, 200).await {
Ok(p) => p,
Err(SaTokenError::StorageError(ref msg)) if msg.contains("Unsupported") => {
tracing::warn!("nonce cleanup skipped: scan unsupported on this backend");
break;
}
Err(e) => return Err(e),
};
for key in page.keys {
if let Some(record) = self.dao.get_object::<NonceRecord>(&key).await? {
if let Ok(dt) = DateTime::parse_from_rfc3339(&record.created_at) {
if dt.with_timezone(&Utc) < cutoff {
self.dao.delete(&key).await?;
removed += 1;
}
}
}
}
if page.next_cursor == 0 {
break;
}
cursor = page.next_cursor;
}
Ok(removed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use sa_token_storage_memory::MemoryStorage;
#[tokio::test]
async fn test_nonce_generation() {
let storage = Arc::new(MemoryStorage::new());
let nonce_mgr = NonceManager::new(storage, 60);
let nonce1 = nonce_mgr.generate();
let nonce2 = nonce_mgr.generate();
assert_ne!(nonce1, nonce2);
assert!(nonce1.starts_with("nonce_"));
}
#[tokio::test]
async fn test_nonce_validation() {
let storage = Arc::new(MemoryStorage::new());
let nonce_mgr = NonceManager::new(storage, 60);
let nonce = nonce_mgr.generate();
assert!(nonce_mgr.validate(&nonce).await.unwrap());
nonce_mgr.store(&nonce, "user_123").await.unwrap();
assert!(!nonce_mgr.validate(&nonce).await.unwrap());
}
#[tokio::test]
async fn test_nonce_validate_and_consume() {
let storage = Arc::new(MemoryStorage::new());
let nonce_mgr = NonceManager::new(storage, 60);
let nonce = nonce_mgr.generate();
nonce_mgr
.validate_and_consume(&nonce, "user_123")
.await
.unwrap();
let result = nonce_mgr.validate_and_consume(&nonce, "user_123").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_nonce_timestamp_check() {
let storage = Arc::new(MemoryStorage::new());
let nonce_mgr = NonceManager::new(storage, 60);
let nonce = nonce_mgr.generate();
assert!(nonce_mgr.check_timestamp(&nonce, 60).unwrap());
assert!(nonce_mgr.check_timestamp(&nonce, 1).unwrap());
}
}