Skip to main content

sa_token_core/
temp_token.rs

1// Author: 金书记 | Author: Jin Shuji
2//! Short-lived tokens that carry a business value (share links, one-shot actions).
3//! 携带业务值的短时令牌(分享链接、一次性操作授权)。
4
5use std::sync::Arc;
6use std::time::Duration;
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11
12use crate::dao::SaTokenDao;
13use crate::error::{SaTokenError, SaTokenResult};
14use crate::token::random_hex;
15use crate::util::StpUtil;
16
17/// Default namespace used in storage keys.
18/// 存储键使用的默认命名空间。
19pub const DEFAULT_NAMESPACE: &str = "default";
20
21/// Persisted temp-token body.
22/// 持久化的临时令牌体。
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct TempTokenRecord {
25    /// Business payload (string or JSON).
26    /// 业务载荷(字符串或 JSON)。
27    pub value: serde_json::Value,
28    /// Namespace for isolation between product lines.
29    /// 产品线隔离用的命名空间。
30    pub namespace: String,
31    /// Absolute expiry; used when the store has not yet evicted the key.
32    /// 绝对过期时间;存储尚未逐出键时仍用它判定。
33    pub expire_at: Option<DateTime<Utc>>,
34}
35
36/// Temp-token operations bound to a Dao.
37/// 绑定 Dao 的临时令牌操作。
38#[derive(Clone)]
39pub struct TempTokenManager {
40    dao: Arc<SaTokenDao>,
41}
42
43impl std::fmt::Debug for TempTokenManager {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        f.write_str("TempTokenManager { .. }")
46    }
47}
48
49impl TempTokenManager {
50    /// Construct from an existing Dao.
51    /// 用已有 Dao 构造。
52    pub fn new(dao: Arc<SaTokenDao>) -> Self {
53        Self { dao }
54    }
55
56    fn ttl(timeout_secs: i64) -> SaTokenResult<Option<Duration>> {
57        if timeout_secs == 0 {
58            return Err(SaTokenError::ConfigError(
59                "temp token timeout must not be 0".into(),
60            ));
61        }
62        if timeout_secs < 0 {
63            Ok(None)
64        } else {
65            Ok(Some(Duration::from_secs(timeout_secs as u64)))
66        }
67    }
68
69    fn expire_at(timeout_secs: i64) -> Option<DateTime<Utc>> {
70        if timeout_secs < 0 {
71            None
72        } else {
73            Some(Utc::now() + chrono::Duration::seconds(timeout_secs))
74        }
75    }
76
77    fn index_digest(value: &str) -> String {
78        let mut h = Sha256::new();
79        h.update(value.as_bytes());
80        hex::encode(h.finalize())
81    }
82
83    /// Create a token. `timeout_secs < 0` means no TTL.
84    /// `record_index` stores a value→token lookup (one value keeps the latest token).
85    ///
86    /// 创建令牌。`timeout_secs < 0` 表示不设 TTL。
87    /// `record_index` 为 true 时写入 value→token 反查(同一 value 只保留最新 token)。
88    pub async fn create(
89        &self,
90        namespace: &str,
91        value: serde_json::Value,
92        timeout_secs: i64,
93        record_index: bool,
94    ) -> SaTokenResult<String> {
95        if namespace.is_empty() {
96            return Err(SaTokenError::ConfigError(
97                "temp token namespace must not be empty".into(),
98            ));
99        }
100        let ttl = Self::ttl(timeout_secs)?;
101        let record = TempTokenRecord {
102            value: value.clone(),
103            namespace: namespace.to_string(),
104            expire_at: Self::expire_at(timeout_secs),
105        };
106        // Retry until the random key is free; 12 is the same default as login uniqueness.
107        // 随机键冲突时重试;次数与登录唯一重试默认值一致。
108        let mut token = String::new();
109        for _ in 0..12 {
110            let candidate = random_hex(32)?;
111            let key = self.dao.keys().temp_token(namespace, &candidate);
112            let raw = self.dao.encode(&record)?;
113            if self.dao.set_if_absent(&key, &raw, ttl).await? {
114                token = candidate;
115                break;
116            }
117        }
118        if token.is_empty() {
119            return Err(SaTokenError::ConfigError(
120                "failed to allocate a unique temp token".into(),
121            ));
122        }
123        if record_index {
124            if let Some(s) = value.as_str() {
125                let ik = self
126                    .dao
127                    .keys()
128                    .temp_index(namespace, &Self::index_digest(s));
129                self.dao.set_string(&ik, &token, ttl).await?;
130            }
131        }
132        Ok(token)
133    }
134
135    /// Parse and return the record. Missing → NotFound; clock past expire_at → Expired.
136    /// 解析记录。缺失为 NotFound;已过 `expire_at` 为 Expired。
137    pub async fn parse(&self, namespace: &str, token: &str) -> SaTokenResult<TempTokenRecord> {
138        if token.is_empty() {
139            return Err(SaTokenError::TempTokenNotFound);
140        }
141        let key = self.dao.keys().temp_token(namespace, token);
142        let rec: TempTokenRecord = self
143            .dao
144            .get_object(&key)
145            .await?
146            .ok_or(SaTokenError::TempTokenNotFound)?;
147        if let Some(exp) = rec.expire_at {
148            if Utc::now() > exp {
149                let _ = self.dao.delete(&key).await;
150                return Err(SaTokenError::TempTokenExpired);
151            }
152        }
153        Ok(rec)
154    }
155
156    /// Lookup the latest token for a string value (requires `record_index` at create).
157    /// 按字符串业务值反查最新 token(创建时需打开 `record_index`)。
158    pub async fn find_token(&self, namespace: &str, value: &str) -> SaTokenResult<String> {
159        let ik = self
160            .dao
161            .keys()
162            .temp_index(namespace, &Self::index_digest(value));
163        self.dao
164            .get_string(&ik)
165            .await?
166            .ok_or(SaTokenError::TempTokenNotFound)
167    }
168
169    /// Delete token and its string-value index when present.
170    /// 删除令牌;若有字符串反查索引则一并删。
171    pub async fn delete(&self, namespace: &str, token: &str) -> SaTokenResult<()> {
172        let key = self.dao.keys().temp_token(namespace, token);
173        if let Ok(Some(rec)) = self.dao.get_object::<TempTokenRecord>(&key).await {
174            if let Some(s) = rec.value.as_str() {
175                let ik = self
176                    .dao
177                    .keys()
178                    .temp_index(namespace, &Self::index_digest(s));
179                let _ = self.dao.delete(&ik).await;
180            }
181        }
182        self.dao.delete(&key).await
183    }
184}
185
186/// StpUtil helpers using the process-global manager.
187/// 使用进程内全局 Manager 的 StpUtil 辅助函数。
188pub async fn create_default(value: impl Into<String>, timeout_secs: i64) -> SaTokenResult<String> {
189    let manager = StpUtil::try_get_manager()?;
190    TempTokenManager::new(manager.dao().clone())
191        .create(
192            DEFAULT_NAMESPACE,
193            serde_json::Value::String(value.into()),
194            timeout_secs,
195            false,
196        )
197        .await
198}
199
200/// Parse a temp token in the default namespace | 解析默认命名空间下的临时令牌
201pub async fn parse_default(token: &str) -> SaTokenResult<TempTokenRecord> {
202    let manager = StpUtil::try_get_manager()?;
203    TempTokenManager::new(manager.dao().clone())
204        .parse(DEFAULT_NAMESPACE, token)
205        .await
206}
207
208/// Delete a temp token in the default namespace | 删除默认命名空间下的临时令牌
209pub async fn delete_default(token: &str) -> SaTokenResult<()> {
210    let manager = StpUtil::try_get_manager()?;
211    TempTokenManager::new(manager.dao().clone())
212        .delete(DEFAULT_NAMESPACE, token)
213        .await
214}