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}