Skip to main content

sa_token_core/
nonce.rs

1// Author: 金书记
2//
3//! Nonce Manager | Nonce 管理器
4//!
5//! Prevents replay attacks by tracking used nonces
6//! 通过跟踪已使用的 nonce 来防止重放攻击
7//!
8//! ## Overview | 概述
9//!
10//! A **nonce** (number used once) is a unique value that can only be used one time,
11//! preventing replay attacks where an attacker reuses a valid request.
12//! **nonce**(一次性数字)是一个只能使用一次的唯一值,防止攻击者重用有效请求的重放攻击。
13//!
14//! ## Integration with Sa-Token | 与 Sa-Token 的集成
15//!
16//! Nonce is used in several Sa-Token scenarios:
17//! Nonce 在 Sa-Token 的多个场景中使用:
18//!
19//! 1. **Login with Nonce** | 带 Nonce 的登录
20//!    - Prevents replay of login requests
21//!    - 防止登录请求的重放
22//!
23//! 2. **Token Creation** | Token 创建
24//!    - Each token can have an associated nonce
25//!    - 每个 token 可以关联一个 nonce
26//!
27//! 3. **OAuth2 / SSO** | OAuth2 / SSO
28//!    - Used in authorization codes and state parameters
29//!    - 用于授权码和状态参数
30//!
31//! 4. **Sensitive Operations** | 敏感操作
32//!    - Password changes, account deletion, etc.
33//!    - 密码修改、账户删除等
34//!
35//! ## Workflow | 工作流程
36//!
37//! ```text
38//! ┌─────────────────────────────────────────────────────────────┐
39//! │                    Nonce Lifecycle                          │
40//! │                    Nonce 生命周期                            │
41//! └─────────────────────────────────────────────────────────────┘
42//!
43//! Client                     NonceManager              Storage
44//! 客户端                     Nonce管理器               存储
45//!   │                             │                        │
46//!   │  1. Request nonce           │                        │
47//!   │  请求 nonce                 │                        │
48//!   │────────────────────────────▶│                        │
49//!   │                             │                        │
50//!   │  2. generate()              │                        │
51//!   │                             │  生成唯一 nonce        │
52//!   │                             │  nonce_TIMESTAMP_UUID  │
53//!   │                             │                        │
54//!   │  3. Return nonce            │                        │
55//!   │  返回 nonce                 │                        │
56//!   │◀────────────────────────────│                        │
57//!   │                             │                        │
58//!   │  4. Use nonce in request    │                        │
59//!   │  在请求中使用 nonce         │                        │
60//!   │────────────────────────────▶│                        │
61//!   │                             │                        │
62//!   │  5. validate_and_consume()  │                        │
63//!   │                             │  Check not used        │
64//!   │                             │  检查未使用             │
65//!   │                             │─────────────────────▶  │
66//!   │                             │  Get nonce key         │
67//!   │                             │                        │
68//!   │                             │  Not found = valid     │
69//!   │                             │  未找到 = 有效          │
70//!   │                             │◀─────────────────────  │
71//!   │                             │                        │
72//!   │                             │  Store nonce (TTL)     │
73//!   │                             │  存储 nonce            │
74//!   │                             │─────────────────────▶  │
75//!   │                             │                        │
76//!   │  6. Request processed       │                        │
77//!   │  请求已处理                 │                        │
78//!   │◀────────────────────────────│                        │
79//!   │                             │                        │
80//!   │  7. Reuse same nonce (ATTACK)                        │
81//!   │  重用相同 nonce(攻击)     │                        │
82//!   │────────────────────────────▶│                        │
83//!   │                             │  Check if used         │
84//!   │                             │  检查是否已使用         │
85//!   │                             │─────────────────────▶  │
86//!   │                             │  Found = already used  │
87//!   │                             │  找到 = 已使用          │
88//!   │                             │◀─────────────────────  │
89//!   │                             │                        │
90//!   │  ❌ Reject (NonceAlreadyUsed)                        │
91//!   │  拒绝(Nonce已使用)         │                        │
92//!   │◀────────────────────────────│                        │
93//!   │                             │                        │
94//!   │                          [After TTL expires]         │
95//!   │                          [TTL 过期后]                 │
96//!   │                             │   Auto cleanup         │
97//!   │                             │   自动清理              │
98//!   │                             │         X──────────────│
99//! ```
100//!
101//! ## Storage Keys | 存储键格式
102//!
103//! ```text
104//! sa:nonce:{nonce_value}
105//!   - Stores: { "login_id": "...", "created_at": "..." }
106//!   - TTL: Configured timeout (default: 60 seconds)
107//!   - Purpose: Mark nonce as used
108//!   
109//!   存储:{ "login_id": "...", "created_at": "..." }
110//!   TTL:配置的超时时间(默认:60秒)
111//!   目的:标记 nonce 为已使用
112//! ```
113//!
114//! ## Security Considerations | 安全考虑
115//!
116//! ```text
117//! 1. ✅ One-Time Use | 一次性使用
118//!    - Nonce can only be used once
119//!    - Stored after first use to prevent reuse
120//!    
121//! 2. ✅ Time-Limited | 时间限制
122//!    - Nonces expire after timeout (default: 60s)
123//!    - Prevents storage bloat
124//!    
125//! 3. ✅ Unique Generation | 唯一生成
126//!    - UUID + timestamp ensures uniqueness
127//!    - Collision probability: negligible
128//!    
129//! 4. ✅ Timestamp Validation | 时间戳验证
130//!    - check_timestamp() validates time window
131//!    - Prevents time-based attacks
132//!    
133//! 5. ✅ Atomic Operations | 原子操作
134//!    - validate_and_consume() is atomic
135//!    - Prevents race conditions
136//! ```
137//!
138//! ## Usage Examples | 使用示例
139//!
140//! ### Example 1: Login with Nonce | 带 Nonce 的登录
141//!
142//! ```rust,ignore
143//! use sa_token_core::manager::SaTokenManager;
144//!
145//! // Client requests nonce
146//! let nonce = nonce_manager.generate();
147//! // Returns: "nonce_1234567890123_abc123def456"
148//!
149//! // Client sends login request with nonce
150//! let token = manager.login_with_options(
151//!     "user_123",
152//!     None,
153//!     None,
154//!     None,
155//!     Some(nonce.clone()),  // ← Nonce here
156//!     None,
157//! ).await?;
158//!
159//! // Server validates and consumes nonce (inside login_with_token_info)
160//! nonce_manager.validate_and_consume(&nonce, "user_123").await?;
161//! // ✅ First use: OK
162//! // ❌ Second use: NonceAlreadyUsed error
163//! ```
164//!
165//! ### Example 2: Sensitive Operation with Nonce | 带 Nonce 的敏感操作
166//!
167//! ```rust,ignore
168//! // Change password with nonce protection
169//! async fn change_password(
170//!     user_id: &str,
171//!     new_password: &str,
172//!     nonce: &str,
173//! ) -> Result<()> {
174//!     // Validate nonce
175//!     nonce_manager.validate_and_consume(nonce, user_id).await?;
176//!     
177//!     // Proceed with password change
178//!     update_password(user_id, new_password).await?;
179//!     
180//!     Ok(())
181//! }
182//! ```
183//!
184//! ## Best Practices | 最佳实践
185//!
186//! 1. **Always generate nonces server-side** | 始终在服务端生成 nonce
187//!    - Don't let clients generate their own nonces
188//!    - 不要让客户端生成自己的 nonce
189//!
190//! 2. **Use appropriate timeout** | 使用适当的超时时间
191//!    - Short timeout (30-60s) for most operations
192//!    - Longer timeout (5-10min) for complex flows
193//!    - 大多数操作使用短超时(30-60秒)
194//!    - 复杂流程使用较长超时(5-10分钟)
195//!
196//! 3. **Validate timestamp** | 验证时间戳
197//!    - Use check_timestamp() for additional validation
198//!    - 使用 check_timestamp() 进行额外验证
199//!
200//! 4. **One nonce per operation** | 每个操作一个 nonce
201//!    - Don't reuse nonces across different operations
202//!    - 不要在不同操作间重用 nonce
203//!
204//! 5. **Combine with other security measures** | 与其他安全措施结合
205//!    - Use nonces WITH authentication, not instead of it
206//!    - 将 nonce 与认证结合使用,而不是替代认证
207//! ```
208
209use crate::config::SaTokenConfig;
210use crate::dao::SaTokenDao;
211use crate::error::{SaTokenError, SaTokenResult};
212use chrono::{DateTime, Utc};
213use sa_token_adapter::storage::SaStorage;
214use serde::{Deserialize, Serialize};
215use std::sync::Arc;
216use uuid::Uuid;
217
218/// Nonce storage record (A2-1) | Nonce 存储记录
219#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
220pub struct NonceRecord {
221    /// Associated login id | 关联登录 ID
222    pub login_id: String,
223    /// Creation time (RFC 3339) | 创建时间(RFC 3339)
224    pub created_at: String,
225}
226
227impl NonceRecord {
228    /// Build a new record for `login_id` | 为 login_id 新建记录
229    pub fn new(login_id: impl Into<String>) -> Self {
230        Self {
231            login_id: login_id.into(),
232            created_at: Utc::now().to_rfc3339(),
233        }
234    }
235}
236
237/// Nonce Manager | Nonce 管理器
238#[derive(Clone)]
239pub struct NonceManager {
240    dao: Arc<SaTokenDao>,
241    timeout: i64,
242}
243
244impl std::fmt::Debug for NonceManager {
245    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246        f.write_str("NonceManager { .. }")
247    }
248}
249
250impl NonceManager {
251    /// Create from Dao | 从 Dao 创建
252    pub fn from_dao(dao: Arc<SaTokenDao>, timeout: i64) -> Self {
253        Self { dao, timeout }
254    }
255
256    /// Legacy wrapper: builds a Dao with default config + given timeout.
257    /// 遗留包装:用默认配置构造 Dao。
258    pub fn new(storage: Arc<dyn SaStorage>, timeout: i64) -> Self {
259        let cfg = SaTokenConfig {
260            nonce_timeout: timeout,
261            ..SaTokenConfig::default()
262        };
263        Self::from_dao(Arc::new(SaTokenDao::new(storage, Arc::new(cfg))), timeout)
264    }
265
266    /// Align serializer with Manager / config (A2-1).
267    /// 与 Manager / 配置对齐序列化器(A2-1)。
268    pub fn with_serializer(
269        mut self,
270        serializer: sa_token_adapter::serializer::SharedSerializer,
271    ) -> Self {
272        let mut cfg = (*self.dao.config()).as_ref().clone();
273        cfg.serializer = serializer;
274        self.dao = Arc::new(SaTokenDao::new(self.dao.storage().clone(), Arc::new(cfg)));
275        self
276    }
277
278    fn ttl(&self) -> Option<std::time::Duration> {
279        if self.timeout > 0 {
280            Some(std::time::Duration::from_secs(self.timeout as u64))
281        } else {
282            None
283        }
284    }
285
286    /// Generate a new nonce | 生成新的 nonce
287    pub fn generate(&self) -> String {
288        format!(
289            "nonce_{}_{}",
290            Utc::now().timestamp_millis(),
291            Uuid::new_v4().simple()
292        )
293    }
294
295    /// Store and mark nonce as used | 存储并标记 nonce 为已使用
296    pub async fn store(&self, nonce: &str, login_id: &str) -> SaTokenResult<()> {
297        let key = self.dao.keys().nonce(nonce);
298        let record = NonceRecord::new(login_id);
299        self.dao.set_object(&key, &record, self.ttl()).await
300    }
301
302    /// Retrieve nonce record for audit (optional) | 检索 nonce 记录(审计可选)
303    pub async fn get_record(&self, nonce: &str) -> SaTokenResult<Option<NonceRecord>> {
304        let key = self.dao.keys().nonce(nonce);
305        self.dao.get_object(&key).await
306    }
307
308    /// Validate nonce and ensure it hasn't been used | 验证 nonce 并确保未被使用
309    pub async fn validate(&self, nonce: &str) -> SaTokenResult<bool> {
310        let key = self.dao.keys().nonce(nonce);
311        Ok(self.dao.get_string(&key).await?.is_none())
312    }
313
314    /// Validate and consume nonce atomically via set_if_absent.
315    /// 通过 set_if_absent 原子校验并消费 nonce。
316    pub async fn validate_and_consume(&self, nonce: &str, login_id: &str) -> SaTokenResult<()> {
317        if nonce.trim().is_empty() {
318            return Err(SaTokenError::InvalidToken("nonce must not be empty".into()));
319        }
320        let key = self.dao.keys().nonce(nonce);
321        let record = NonceRecord::new(login_id);
322        let raw = self.dao.encode(&record)?;
323        let occupied = self.dao.set_if_absent(&key, &raw, self.ttl()).await?;
324        if !occupied {
325            return Err(SaTokenError::NonceAlreadyUsed);
326        }
327        Ok(())
328    }
329
330    /// Extract timestamp from nonce and check if it's within valid time window
331    /// 从 nonce 中提取时间戳并检查是否在有效时间窗口内
332    pub fn check_timestamp(&self, nonce: &str, window_seconds: i64) -> SaTokenResult<bool> {
333        let parts: Vec<&str> = nonce.split('_').collect();
334        if parts.len() < 3 {
335            return Err(SaTokenError::InvalidNonceFormat);
336        }
337        let timestamp_ms: i64 = parts
338            .get(1)
339            .ok_or(SaTokenError::InvalidNonceFormat)?
340            .parse()
341            .map_err(|_| SaTokenError::InvalidNonceTimestamp)?;
342        let now_ms = Utc::now().timestamp_millis();
343        let age_seconds = (now_ms - timestamp_ms) / 1000;
344        Ok(age_seconds >= 0 && age_seconds <= window_seconds)
345    }
346
347    /// Scan and remove expired nonce records | 扫描并删除过期 nonce 记录
348    pub async fn cleanup_expired(&self) -> SaTokenResult<usize> {
349        if self.timeout <= 0 {
350            return Ok(0);
351        }
352        let pattern = self.dao.keys().scan_pattern("nonce", None);
353        let mut removed = 0usize;
354        let mut cursor = 0u64;
355        let cutoff = Utc::now() - chrono::Duration::seconds(self.timeout);
356
357        loop {
358            let page = match self.dao.scan(&pattern, cursor, 200).await {
359                Ok(p) => p,
360                Err(SaTokenError::StorageError(ref msg)) if msg.contains("Unsupported") => {
361                    tracing::warn!("nonce cleanup skipped: scan unsupported on this backend");
362                    break;
363                }
364                Err(e) => return Err(e),
365            };
366
367            for key in page.keys {
368                if let Some(record) = self.dao.get_object::<NonceRecord>(&key).await? {
369                    if let Ok(dt) = DateTime::parse_from_rfc3339(&record.created_at) {
370                        if dt.with_timezone(&Utc) < cutoff {
371                            self.dao.delete(&key).await?;
372                            removed += 1;
373                        }
374                    }
375                }
376            }
377            if page.next_cursor == 0 {
378                break;
379            }
380            cursor = page.next_cursor;
381        }
382        Ok(removed)
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389    use sa_token_storage_memory::MemoryStorage;
390
391    #[tokio::test]
392    async fn test_nonce_generation() {
393        let storage = Arc::new(MemoryStorage::new());
394        let nonce_mgr = NonceManager::new(storage, 60);
395
396        let nonce1 = nonce_mgr.generate();
397        let nonce2 = nonce_mgr.generate();
398
399        assert_ne!(nonce1, nonce2);
400        assert!(nonce1.starts_with("nonce_"));
401    }
402
403    #[tokio::test]
404    async fn test_nonce_validation() {
405        let storage = Arc::new(MemoryStorage::new());
406        let nonce_mgr = NonceManager::new(storage, 60);
407
408        let nonce = nonce_mgr.generate();
409
410        // First validation should succeed
411        assert!(nonce_mgr.validate(&nonce).await.unwrap());
412
413        // Store the nonce
414        nonce_mgr.store(&nonce, "user_123").await.unwrap();
415
416        // Second validation should fail (already used)
417        assert!(!nonce_mgr.validate(&nonce).await.unwrap());
418    }
419
420    #[tokio::test]
421    async fn test_nonce_validate_and_consume() {
422        let storage = Arc::new(MemoryStorage::new());
423        let nonce_mgr = NonceManager::new(storage, 60);
424
425        let nonce = nonce_mgr.generate();
426
427        // First use should succeed
428        nonce_mgr
429            .validate_and_consume(&nonce, "user_123")
430            .await
431            .unwrap();
432
433        // Second use should fail
434        let result = nonce_mgr.validate_and_consume(&nonce, "user_123").await;
435        assert!(result.is_err());
436    }
437
438    #[tokio::test]
439    async fn test_nonce_timestamp_check() {
440        let storage = Arc::new(MemoryStorage::new());
441        let nonce_mgr = NonceManager::new(storage, 60);
442
443        let nonce = nonce_mgr.generate();
444
445        // Should be within 60 seconds
446        assert!(nonce_mgr.check_timestamp(&nonce, 60).unwrap());
447
448        // Should also be within 1 second
449        assert!(nonce_mgr.check_timestamp(&nonce, 1).unwrap());
450    }
451}