Skip to main content

sa_token_core/
keys.rs

1// Author: 金书记 | Author: Jin Shuji
2//
3//! Unified Storage Key Construction | 存储键统一构造(P3)
4//!
5//! Single source of truth for **all** sa-token storage keys.
6//! sa-token **全部**存储键的唯一来源。
7//!
8//! ## Why This Module Exists | 本模块存在的理由
9//!
10//! Before this module, storage keys were built via ad-hoc `format!` calls scattered
11//! across 10+ files. Any prefix/layout change required touching every call site,
12//! and a single typo caused silent data isolation bugs (e.g. `login_type="admin"`
13//! writing to one key but reading from another).
14//! 在本模块之前,存储键由散落在 10+ 个文件中的临时 `format!` 拼接而成。
15//! 任何前缀/布局变更都需改动所有调用点,一个拼写错误就会造成静默的数据隔离 bug
16//! (例如 `login_type="admin"` 写入一个键、读取另一个键)。
17//!
18//! ## Two Layouts | 两种布局
19//!
20//! | Layout | Format | Purpose |
21//! |--------|--------|---------|
22//! | [`SaKeyLayout::ThreeSegment`] | `{prefix}{category}:{account_ns}` | Rust default, zero migration for existing data \| Rust 默认,存量数据零迁移 |
23//! | [`SaKeyLayout::JavaFourSegment`] | `{token_name}:{login_type}:{category}:{id}` | Four-segment layout for sharing a keyspace with another service. / 四段布局,便于与另一套服务共用键空间。 |
24//!
25//! ## Key Categories | 键分类
26//!
27//! ```text
28//! Global keys (token is globally unique, no account isolation needed)
29//! 全局键(token 全局唯一,无需账号体系隔离)
30//!   token_info / token_id_mapping / token_session / last_active / nonce / refresh
31//!
32//! Account-scoped keys (must be isolated per login_type)
33//! 账号域键(必须按 login_type 隔离)
34//!   login_token / login_token_index / account_session / permission / role
35//!   disable / refresh_user_index
36//! ```
37//!
38//! ## Type Safety: LoginId vs AccountNs (A3-2) | 类型安全:LoginId 与 AccountNs(A3-2)
39//!
40//! A historic bug class: callers computed `account_ns()` first, then passed the
41//! **already-namespaced** string into an API expecting a **raw** login_id, causing
42//! double namespacing under `JavaFourSegment`. The [`LoginId`] / [`AccountNs`]
43//! newtypes make this a **compile error** instead of a runtime data bug.
44//! 一类历史 bug:调用方先算出 `account_ns()`,再把**已命名空间化**的字符串传给期望
45//! **裸** login_id 的 API,在 `JavaFourSegment` 下造成双重命名空间化。
46//! [`LoginId`] / [`AccountNs`] newtype 将其从运行时数据 bug 变为**编译错误**。
47//!
48//! ## Performance Notes (A3-14, A3-15) | 性能说明(A3-14、A3-15)
49//!
50//! - `root: Arc<str>` — cloning [`SaKeys`] is a refcount bump, not a heap copy.
51//!   `root: Arc<str>` — 克隆 [`SaKeys`] 是引用计数递增,而非堆拷贝。
52//! - Key building uses `String::with_capacity` + `write!` — **one** allocation per key.
53//!   键构造使用 `String::with_capacity` + `write!` — 每个键**一次**分配。
54
55use std::fmt::Write as _;
56use std::sync::Arc;
57
58use crate::config::SaTokenConfig;
59
60// ==================== Login Type Constants (A3-13) | 账号体系常量(A3-13) ====================
61
62/// Default login type in Rust sa-token | Rust sa-token 中的默认账号体系
63///
64/// Normalized to a bare `login_id` by [`SaKeys::account_ns`] for backward compatibility.
65/// 由 [`SaKeys::account_ns`] 归一为裸 `login_id`,以保持向后兼容。
66pub const LOGIN_TYPE_DEFAULT: &str = "default";
67
68/// Canonical default account-system id `login`, stored as a bare login_id.
69/// 默认账号体系 id `login`,键中为裸 login_id。
70///
71/// Also normalized to a bare `login_id`, so `"default"` and `"login"`
72/// produce identical three-segment keys.
73/// 同样归一为裸 `login_id`,因此 `"default"` 与 `"login"`
74/// 在三段式下产出完全相同的键。
75pub const LOGIN_TYPE_LOGIN: &str = "login";
76
77/// Login type used by the SSO server side | SSO 服务端使用的账号体系
78pub const LOGIN_TYPE_SSO: &str = "sso";
79
80/// Login type used by the SSO client side | SSO 客户端使用的账号体系
81pub const LOGIN_TYPE_SSO_CLIENT: &str = "sso_client";
82
83/// Escape sequence for a literal `:` inside a `login_id` (A3-16)
84/// `login_id` 内字面量 `:` 的转义序列(A3-16)
85const COLON_ESCAPE: &str = "%3A";
86
87/// Maximum accepted `login_id` byte length | 可接受的 `login_id` 最大字节长度
88const MAX_LOGIN_ID_LEN: usize = 512;
89
90// ==================== Key Error | 键构造错误 ====================
91
92/// Storage key construction error | 存储键构造错误
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum KeyError {
95    /// `login_id` is empty | `login_id` 为空
96    EmptyLoginId,
97
98    /// `login_id` exceeds [`MAX_LOGIN_ID_LEN`] bytes | `login_id` 超过 [`MAX_LOGIN_ID_LEN`] 字节
99    LoginIdTooLong {
100        /// Actual byte length | 实际字节长度
101        actual: usize,
102        /// Maximum allowed byte length | 允许的最大字节长度
103        max: usize,
104    },
105
106    /// A namespaced-id API was called under a layout that cannot support it
107    /// 在无法支持的布局下调用了「已命名空间化 id」API
108    NamespacedIdUnsupportedByLayout {
109        /// The API that was called | 被调用的 API 名
110        api: &'static str,
111    },
112}
113
114impl std::fmt::Display for KeyError {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        match self {
117            Self::EmptyLoginId => write!(f, "login_id must not be empty"),
118            Self::LoginIdTooLong { actual, max } => write!(
119                f,
120                "login_id is too long: {actual} bytes exceeds the maximum of {max} bytes"
121            ),
122            Self::NamespacedIdUnsupportedByLayout { api } => write!(
123                f,
124                "{api} requires SaKeyLayout::ThreeSegment; use the (login_type, login_id) variant instead"
125            ),
126        }
127    }
128}
129
130impl std::error::Error for KeyError {}
131
132// ==================== LoginId / AccountNs Newtypes (A3-2) ====================
133
134/// A **raw** account identifier, not yet namespaced | **裸**账号标识符,尚未命名空间化
135#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
136#[repr(transparent)]
137pub struct LoginId(String);
138
139impl LoginId {
140    /// Wraps a raw account id without validation | 包装裸账号 id,不做校验
141    #[inline]
142    pub fn new(id: impl Into<String>) -> Self {
143        Self(id.into())
144    }
145
146    /// Wraps a raw account id, rejecting empty / over-long values (A3-16)
147    /// 包装裸账号 id,拒绝空值/超长值(A3-16)
148    pub fn try_new(id: impl Into<String>) -> Result<Self, KeyError> {
149        let id = id.into();
150        SaKeys::validate_login_id(&id)?;
151        Ok(Self(id))
152    }
153
154    /// Borrows the underlying raw id | 借用底层裸 id
155    #[inline]
156    pub fn as_str(&self) -> &str {
157        &self.0
158    }
159
160    /// Unwraps into the owned `String` | 解包为拥有所有权的 `String`
161    #[inline]
162    pub fn into_inner(self) -> String {
163        self.0
164    }
165}
166
167impl std::fmt::Display for LoginId {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        f.write_str(&self.0)
170    }
171}
172
173impl AsRef<str> for LoginId {
174    #[inline]
175    fn as_ref(&self) -> &str {
176        &self.0
177    }
178}
179
180impl From<&str> for LoginId {
181    #[inline]
182    fn from(value: &str) -> Self {
183        Self(value.to_string())
184    }
185}
186
187impl From<String> for LoginId {
188    #[inline]
189    fn from(value: String) -> Self {
190        Self(value)
191    }
192}
193
194/// An **already-namespaced** account identifier | **已命名空间化**的账号标识符
195#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
196#[repr(transparent)]
197pub struct AccountNs(String);
198
199impl AccountNs {
200    /// Wraps a value that is **known** to be already namespaced
201    /// 包装一个**已知**已命名空间化的值
202    #[inline]
203    pub fn from_trusted(ns: impl Into<String>) -> Self {
204        Self(ns.into())
205    }
206
207    /// Borrows the underlying namespaced id | 借用底层已命名空间化的 id
208    #[inline]
209    pub fn as_str(&self) -> &str {
210        &self.0
211    }
212
213    /// Unwraps into the owned `String` | 解包为拥有所有权的 `String`
214    #[inline]
215    pub fn into_inner(self) -> String {
216        self.0
217    }
218}
219
220impl std::fmt::Display for AccountNs {
221    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222        f.write_str(&self.0)
223    }
224}
225
226impl AsRef<str> for AccountNs {
227    #[inline]
228    fn as_ref(&self) -> &str {
229        &self.0
230    }
231}
232
233// ==================== Key Layout | 键布局策略 ====================
234
235/// Storage key layout strategy | 存储键布局策略
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
237pub enum SaKeyLayout {
238    /// Three-segment layout (Rust default) | 三段式布局(Rust 默认)
239    ///
240    /// Format: `{storage_key_prefix}{category}:{account_ns}`
241    /// 格式:`{storage_key_prefix}{category}:{account_ns}`
242    #[default]
243    ThreeSegment,
244
245    /// Four-segment layout | 四段式布局
246    ///
247    /// Format: `{token_name}:{login_type}:{category}:{login_id}`
248    /// 格式:`{token_name}:{login_type}:{category}:{login_id}`
249    JavaFourSegment,
250}
251
252// ==================== SaKeys | 键构造器 ====================
253
254/// Storage key builder — the single source of truth for key schema
255/// 存储键构造器 —— 键 schema 的唯一来源
256#[derive(Debug, Clone)]
257pub struct SaKeys {
258    /// Key root | 键根
259    root: Arc<str>,
260    /// Active layout strategy | 生效的布局策略
261    layout: SaKeyLayout,
262    /// ThreeSegment 下 `"{root}token:"`,避免 scan 解析重复分配。
263    /// Cached `"{root}token:"` for ThreeSegment scan parsing.
264    token_colon: Arc<str>,
265}
266
267impl SaKeys {
268    /// Creates a builder with the default [`SaKeyLayout::ThreeSegment`] layout
269    /// 使用默认 [`SaKeyLayout::ThreeSegment`] 布局创建构造器
270    pub fn new(prefix: impl AsRef<str>) -> Self {
271        let root: Arc<str> = Arc::from(prefix.as_ref());
272        let token_colon = Arc::from(format!("{root}token:"));
273        Self {
274            root,
275            layout: SaKeyLayout::ThreeSegment,
276            token_colon,
277        }
278    }
279
280    /// Creates a builder with an explicit layout | 使用显式布局创建构造器
281    pub fn with_layout(root: impl AsRef<str>, layout: SaKeyLayout) -> Self {
282        let root: Arc<str> = match layout {
283            SaKeyLayout::ThreeSegment => Arc::from(root.as_ref()),
284            SaKeyLayout::JavaFourSegment => Arc::from(root.as_ref().trim_end_matches(':')),
285        };
286        let token_colon = Arc::from(format!("{root}token:"));
287        Self {
288            root,
289            layout,
290            token_colon,
291        }
292    }
293
294    /// Builds from config, honouring `key_layout` (A3-1) | 从配置构建,遵循 `key_layout`(A3-1)
295    pub fn from_config(config: &SaTokenConfig) -> Self {
296        match config.key_layout {
297            SaKeyLayout::ThreeSegment => {
298                Self::with_layout(&config.storage_key_prefix, SaKeyLayout::ThreeSegment)
299            }
300            SaKeyLayout::JavaFourSegment => {
301                Self::with_layout(&config.token_name, SaKeyLayout::JavaFourSegment)
302            }
303        }
304    }
305
306    /// Returns the key root | 返回键根
307    #[inline]
308    pub fn prefix(&self) -> &str {
309        &self.root
310    }
311
312    /// Returns the active layout | 返回生效的布局
313    #[inline]
314    pub fn layout(&self) -> SaKeyLayout {
315        self.layout
316    }
317
318    #[inline]
319    fn is_java(&self) -> bool {
320        matches!(self.layout, SaKeyLayout::JavaFourSegment)
321    }
322
323    /// Normalizes `(login_type, login_id)` into a single key segment (A3-2, A3-16)
324    /// 将 `(login_type, login_id)` 归一为单个键段(A3-2、A3-16)
325    pub fn account_ns(login_type: &str, login_id: &LoginId) -> AccountNs {
326        let id = login_id.as_str();
327
328        if Self::is_default_login_type(login_type) {
329            return AccountNs(id.to_string());
330        }
331
332        let needs_escape = id.contains(':');
333        let escaped_extra = if needs_escape {
334            id.matches(':').count() * (COLON_ESCAPE.len() - 1)
335        } else {
336            0
337        };
338
339        let mut out = String::with_capacity(login_type.len() + 1 + id.len() + escaped_extra);
340        out.push_str(login_type);
341        out.push(':');
342        if needs_escape {
343            Self::push_escaped(&mut out, id);
344        } else {
345            out.push_str(id);
346        }
347        AccountNs(out)
348    }
349
350    /// Returns `true` for login types normalized to the bare id
351    /// 对归一为裸 id 的账号体系返回 `true`
352    #[inline]
353    pub fn is_default_login_type(login_type: &str) -> bool {
354        login_type.is_empty() || login_type == LOGIN_TYPE_DEFAULT || login_type == LOGIN_TYPE_LOGIN
355    }
356
357    #[inline]
358    fn push_escaped(out: &mut String, src: &str) {
359        for ch in src.chars() {
360            if ch == ':' {
361                out.push_str(COLON_ESCAPE);
362            } else {
363                out.push(ch);
364            }
365        }
366    }
367
368    /// Validates a `login_id` before it is used in a key (A3-16)
369    /// 在 `login_id` 用于构造键之前校验它(A3-16)
370    pub fn validate_login_id(login_id: &str) -> Result<(), KeyError> {
371        if login_id.is_empty() {
372            return Err(KeyError::EmptyLoginId);
373        }
374        if login_id.len() > MAX_LOGIN_ID_LEN {
375            return Err(KeyError::LoginIdTooLong {
376                actual: login_id.len(),
377                max: MAX_LOGIN_ID_LEN,
378            });
379        }
380        Ok(())
381    }
382
383    fn build_global(&self, category: &str, id: &str, login_type: Option<&str>) -> String {
384        match self.layout {
385            SaKeyLayout::ThreeSegment => {
386                let mut out =
387                    String::with_capacity(self.root.len() + category.len() + 1 + id.len());
388                out.push_str(&self.root);
389                out.push_str(category);
390                out.push(':');
391                out.push_str(id);
392                out
393            }
394            SaKeyLayout::JavaFourSegment => {
395                let lt = login_type.unwrap_or(LOGIN_TYPE_LOGIN);
396                let mut out = String::with_capacity(
397                    self.root.len() + 1 + lt.len() + 1 + category.len() + 1 + id.len(),
398                );
399                let _ = write!(out, "{}:{}:{}:{}", self.root, lt, category, id);
400                out
401            }
402        }
403    }
404
405    fn build_account(&self, category: &str, login_type: &str, login_id: &str) -> String {
406        match self.layout {
407            SaKeyLayout::ThreeSegment => {
408                if Self::is_default_login_type(login_type) {
409                    let mut out = String::with_capacity(
410                        self.root.len() + category.len() + 1 + login_id.len(),
411                    );
412                    out.push_str(&self.root);
413                    out.push_str(category);
414                    out.push(':');
415                    out.push_str(login_id);
416                    return out;
417                }
418
419                let escaped_extra = login_id.matches(':').count() * (COLON_ESCAPE.len() - 1);
420                let mut out = String::with_capacity(
421                    self.root.len()
422                        + category.len()
423                        + 1
424                        + login_type.len()
425                        + 1
426                        + login_id.len()
427                        + escaped_extra,
428                );
429                out.push_str(&self.root);
430                out.push_str(category);
431                out.push(':');
432                out.push_str(login_type);
433                out.push(':');
434                Self::push_escaped(&mut out, login_id);
435                out
436            }
437            SaKeyLayout::JavaFourSegment => {
438                let lt = if login_type.is_empty() {
439                    LOGIN_TYPE_LOGIN
440                } else {
441                    login_type
442                };
443                let mut out = String::with_capacity(
444                    self.root.len() + 1 + lt.len() + 1 + category.len() + 1 + login_id.len(),
445                );
446                let _ = write!(out, "{}:{}:{}:{}", self.root, lt, category, login_id);
447                out
448            }
449        }
450    }
451
452    fn build_from_ns(
453        &self,
454        category: &str,
455        ns: &AccountNs,
456        api: &'static str,
457    ) -> Result<String, KeyError> {
458        if self.is_java() {
459            return Err(KeyError::NamespacedIdUnsupportedByLayout { api });
460        }
461        Ok(self.build_global(category, ns.as_str(), None))
462    }
463
464    /// Deprecated escape hatch kept for legacy call sites (A3-10)
465    /// 为存量调用点保留的已弃用逃生舱(A3-10)
466    #[deprecated(
467        since = "0.1.19",
468        note = "Use a named key method (token_info / login_token / ...) so the key layout is respected"
469    )]
470    pub fn make_key(&self, suffix: &str, id: &str) -> String {
471        let mut out = String::with_capacity(self.root.len() + suffix.len() + id.len());
472        out.push_str(&self.root);
473        out.push_str(suffix);
474        out.push_str(id);
475        out
476    }
477
478    // ==================== Token Global Keys | Token 全局键 ====================
479
480    /// Token → login_id mapping key | Token → login_id 映射键
481    #[inline]
482    pub fn token_info(&self, token: &str) -> String {
483        self.build_global("token", token, None)
484    }
485
486    /// Token key for a specific account system (A3-4) | 指定账号体系的 Token 键(A3-4)
487    #[inline]
488    pub fn token_info_with_type(&self, login_type: &str, token: &str) -> String {
489        self.build_global("token", token, Some(login_type))
490    }
491
492    /// Reverse token → id mapping key (Rust-specific) | 反向 token → id 映射键(Rust 独有)
493    #[inline]
494    pub fn token_id_mapping(&self, token: &str) -> String {
495        self.build_global("token-id", token, None)
496    }
497
498    /// Token-Session key | Token-Session 键
499    #[inline]
500    pub fn token_session(&self, token: &str) -> String {
501        self.build_global("token-session", token, None)
502    }
503
504    /// Token-Session key for a specific account system (A3-4)
505    /// 指定账号体系的 Token-Session 键(A3-4)
506    #[inline]
507    pub fn token_session_with_type(&self, login_type: &str, token: &str) -> String {
508        self.build_global("token-session", token, Some(login_type))
509    }
510
511    /// Last-active timestamp key | 最后活跃时间键
512    #[inline]
513    pub fn last_active(&self, token: &str) -> String {
514        self.build_global("last-active", token, None)
515    }
516
517    /// Last-active key for a specific account system (A3-4)
518    /// 指定账号体系的最后活跃时间键(A3-4)
519    #[inline]
520    pub fn last_active_with_type(&self, login_type: &str, token: &str) -> String {
521        self.build_global("last-active", token, Some(login_type))
522    }
523
524    // ==================== Account-Scoped Keys | 账号域键 ====================
525
526    /// login_id → token mapping key (Rust-specific) | login_id → token 映射键(Rust 独有)
527    #[inline]
528    pub fn login_token(&self, login_type: &str, login_id: &str) -> String {
529        self.build_account("login:token", login_type, login_id)
530    }
531
532    /// Multi-device token index key (Rust-specific) | 多设备 token 索引键(Rust 独有)
533    #[inline]
534    pub fn login_token_index(&self, login_type: &str, login_id: &str) -> String {
535        self.build_account("login:tokens", login_type, login_id)
536    }
537
538    /// Account-Session key | Account-Session 键
539    #[inline]
540    pub fn account_session(&self, login_type: &str, login_id: &str) -> String {
541        self.build_account("session", login_type, login_id)
542    }
543
544    /// Account-Session key from an already-namespaced id (A3-2)
545    /// 从已命名空间化 id 构造 Account-Session 键(A3-2)
546    #[inline]
547    pub fn session_by_ns(&self, ns: &AccountNs) -> Result<String, KeyError> {
548        self.build_from_ns("session", ns, "SaKeys::session_by_ns")
549    }
550
551    /// Permission list key | 权限列表键
552    #[inline]
553    pub fn permission(&self, login_type: &str, login_id: &str) -> String {
554        self.build_account("permission", login_type, login_id)
555    }
556
557    /// Role list key | 角色列表键
558    #[inline]
559    pub fn role(&self, login_type: &str, login_id: &str) -> String {
560        self.build_account("role", login_type, login_id)
561    }
562
563    /// Account ban (disable) key | 账号封禁键
564    pub fn disable(&self, login_type: &str, login_id: &str, service: &str) -> String {
565        match self.layout {
566            SaKeyLayout::ThreeSegment => {
567                let mut out = self.build_account("disable", login_type, login_id);
568                out.push(':');
569                out.push_str(service);
570                out
571            }
572            SaKeyLayout::JavaFourSegment => {
573                let lt = if login_type.is_empty() {
574                    LOGIN_TYPE_LOGIN
575                } else {
576                    login_type
577                };
578                let mut out = String::with_capacity(
579                    self.root.len() + 1 + lt.len() + 9 + service.len() + 1 + login_id.len(),
580                );
581                let _ = write!(out, "{}:{}:disable:{}:{}", self.root, lt, service, login_id);
582                out
583            }
584        }
585    }
586
587    /// Ban key from an already-namespaced id (A3-2) | 从已命名空间化 id 构造封禁键(A3-2)
588    pub fn disable_by_ns(&self, ns: &AccountNs, service: &str) -> Result<String, KeyError> {
589        if self.is_java() {
590            return Err(KeyError::NamespacedIdUnsupportedByLayout {
591                api: "SaKeys::disable_by_ns",
592            });
593        }
594        let mut out = self.build_global("disable", ns.as_str(), None);
595        out.push(':');
596        out.push_str(service);
597        Ok(out)
598    }
599
600    /// Second-factor (safe) verification key | 二级认证键
601    pub fn safe(&self, token: &str, service: &str) -> String {
602        self.safe_with_type(LOGIN_TYPE_LOGIN, token, service)
603    }
604
605    /// Second-factor key for a specific account system | 指定账号体系的二级认证键
606    pub fn safe_with_type(&self, login_type: &str, token: &str, service: &str) -> String {
607        match self.layout {
608            SaKeyLayout::ThreeSegment => {
609                let mut out = self.build_global("safe", token, None);
610                out.push(':');
611                out.push_str(service);
612                out
613            }
614            SaKeyLayout::JavaFourSegment => {
615                let lt = if login_type.is_empty() {
616                    LOGIN_TYPE_LOGIN
617                } else {
618                    login_type
619                };
620                let mut out = String::with_capacity(
621                    self.root.len() + 1 + lt.len() + 6 + service.len() + 1 + token.len(),
622                );
623                let _ = write!(out, "{}:{}:safe:{}:{}", self.root, lt, service, token);
624                out
625            }
626        }
627    }
628
629    // ==================== Nonce / Refresh / OAuth2 / SSO / Online / Distributed ====================
630
631    /// Nonce replay-protection key | Nonce 防重放键
632    #[inline]
633    pub fn nonce(&self, nonce_value: &str) -> String {
634        self.build_global("nonce", nonce_value, None)
635    }
636
637    /// Refresh token key | Refresh Token 键
638    #[inline]
639    pub fn refresh(&self, refresh_token: &str) -> String {
640        self.build_global("refresh", refresh_token, None)
641    }
642
643    /// Per-account refresh token index key | 按账号的 Refresh Token 索引键
644    #[inline]
645    pub fn refresh_user_index(&self, login_type: &str, login_id: &str) -> String {
646        self.build_account("refresh:user", login_type, login_id)
647    }
648
649    /// Refresh index key from an already-namespaced id (A3-2)
650    /// 从已命名空间化 id 构造 Refresh 索引键(A3-2)
651    #[inline]
652    pub fn refresh_user_index_by_ns(&self, ns: &AccountNs) -> Result<String, KeyError> {
653        self.build_from_ns("refresh:user", ns, "SaKeys::refresh_user_index_by_ns")
654    }
655
656    /// OAuth2 client registration key | OAuth2 客户端注册键
657    #[inline]
658    pub fn oauth2_client(&self, client_id: &str) -> String {
659        self.build_global("oauth2:client", client_id, None)
660    }
661
662    /// OAuth2 authorization code key | OAuth2 授权码键
663    #[inline]
664    pub fn oauth2_code(&self, code: &str) -> String {
665        self.build_global("oauth2:code", code, None)
666    }
667
668    /// OAuth2 access token key | OAuth2 访问令牌键
669    #[inline]
670    pub fn oauth2_token(&self, access_token: &str) -> String {
671        self.build_global("oauth2:token", access_token, None)
672    }
673
674    /// OAuth2 refresh token key | OAuth2 刷新令牌键
675    #[inline]
676    pub fn oauth2_refresh(&self, refresh_token: &str) -> String {
677        self.build_global("oauth2:refresh", refresh_token, None)
678    }
679
680    /// SSO ticket key | SSO 票据键
681    #[inline]
682    pub fn sso_ticket(&self, ticket_id: &str) -> String {
683        self.build_global("sso:ticket", ticket_id, None)
684    }
685
686    /// SSO session key | SSO 会话键
687    #[inline]
688    pub fn sso_session(&self, login_id: &str) -> String {
689        self.build_global("sso:session", login_id, None)
690    }
691
692    /// SSO login-token key | SSO 登录令牌键
693    #[inline]
694    pub fn sso_login_token(&self, login_type: &str, login_id: &str) -> String {
695        self.login_token(login_type, login_id)
696    }
697
698    /// Online-user record key | 在线用户记录键
699    #[inline]
700    pub fn online(&self, login_id: &str, token: &str) -> String {
701        let mut out = self.build_global("online", login_id, None);
702        out.push(':');
703        out.push_str(token);
704        out
705    }
706
707    /// Online-user record key for a specific account system (A3-13)
708    /// 指定账号体系的在线用户记录键(A3-13)
709    #[inline]
710    pub fn online_with_type(&self, login_type: &str, login_id: &str, token: &str) -> String {
711        let mut out = self.build_account("online", login_type, login_id);
712        out.push(':');
713        out.push_str(token);
714        out
715    }
716
717    /// Online-user index key | 在线用户索引键
718    #[inline]
719    pub fn online_index(&self, login_id: &str) -> String {
720        self.build_global("online:index", login_id, None)
721    }
722
723    /// Online-user index key for a specific account system.
724    /// 指定账号体系的在线用户索引键。
725    #[inline]
726    pub fn online_index_with_type(&self, login_type: &str, login_id: &str) -> String {
727        self.build_account("online:index", login_type, login_id)
728    }
729
730    /// Global unique set of currently online login ids (list primitive).
731    /// 当前在线账号 ID 的全局去重集合(走列表原语)。
732    #[inline]
733    pub fn online_users_set(&self) -> String {
734        self.build_global("online", "users", None)
735    }
736
737    /// Distributed session key | 分布式会话键
738    #[inline]
739    pub fn distributed_session(&self, session_id: &str) -> String {
740        self.build_global("dsession", session_id, None)
741    }
742
743    /// Distributed session index key | 分布式会话索引键
744    #[inline]
745    pub fn distributed_session_index(&self, login_id: &str) -> String {
746        self.build_global("dsession:index", login_id, None)
747    }
748
749    /// Distributed service credential key | 分布式服务凭证键
750    #[inline]
751    pub fn distributed_service(&self, service_id: &str) -> String {
752        self.build_global("dservice", service_id, None)
753    }
754
755    /// Current Same-Token storage key.
756    /// 当前 Same-Token 存储键。
757    #[inline]
758    pub fn same_token(&self) -> String {
759        self.build_global("var", "same-token", None)
760    }
761
762    /// Previous Same-Token storage key (grace window).
763    /// 上一次 Same-Token 存储键(宽限期)。
764    #[inline]
765    pub fn same_token_past(&self) -> String {
766        self.build_global("var", "same-token-past", None)
767    }
768
769    /// Request-sign nonce occupancy key (not the login nonce space).
770    /// 请求签名 nonce 占位键(与登录 nonce 键空间分离)。
771    #[inline]
772    pub fn sign_nonce(&self, nonce: &str) -> String {
773        self.build_global("sign-nonce", nonce, None)
774    }
775
776    /// Temp-token body key.
777    /// 临时令牌体键。
778    #[inline]
779    pub fn temp_token(&self, namespace: &str, token: &str) -> String {
780        let mut cat = String::from("temp-token:");
781        cat.push_str(namespace);
782        self.build_global(&cat, token, None)
783    }
784
785    /// Temp-token reverse index (digest of the string value).
786    /// 临时令牌反查索引(字符串 value 的摘要)。
787    #[inline]
788    pub fn temp_index(&self, namespace: &str, value_digest: &str) -> String {
789        let mut cat = String::from("temp-index:");
790        cat.push_str(namespace);
791        self.build_global(&cat, value_digest, None)
792    }
793
794    // ==================== Scan & Parse (A3-11, A3-12) | 扫描与解析(A3-11、A3-12) ====================
795
796    /// Returns the key prefix for a category, layout-aware (A3-11)
797    /// 返回某分类的键前缀,布局感知(A3-11)
798    pub fn category_prefix(&self, category: &str, login_type: Option<&str>) -> String {
799        match self.layout {
800            SaKeyLayout::ThreeSegment => {
801                let mut out = String::with_capacity(self.root.len() + category.len() + 1);
802                out.push_str(&self.root);
803                out.push_str(category);
804                out.push(':');
805                out
806            }
807            SaKeyLayout::JavaFourSegment => {
808                let lt = login_type.unwrap_or(LOGIN_TYPE_LOGIN);
809                let mut out =
810                    String::with_capacity(self.root.len() + 1 + lt.len() + 1 + category.len() + 1);
811                let _ = write!(out, "{}:{}:{}:", self.root, lt, category);
812                out
813            }
814        }
815    }
816
817    /// Token key prefix, for scan-and-strip workflows (A3-11)
818    /// Token 键前缀,用于「扫描后剥离」工作流(A3-11)
819    #[inline]
820    pub fn token_key_prefix(&self, login_type: Option<&str>) -> String {
821        self.category_prefix("token", login_type)
822    }
823
824    /// Glob pattern matching every token key (A3-11) | 匹配所有 token 键的 glob 模式(A3-11)
825    pub fn token_scan_pattern(&self, login_type: Option<&str>) -> String {
826        let mut out = self.token_key_prefix(login_type);
827        out.push('*');
828        out
829    }
830
831    /// Glob pattern for any category (A3-11) | 任意分类的 glob 模式(A3-11)
832    pub fn scan_pattern(&self, category: &str, login_type: Option<&str>) -> String {
833        let mut out = self.category_prefix(category, login_type);
834        out.push('*');
835        out
836    }
837
838    /// Extracts the token value from a scanned token key (A3-12)
839    /// 从扫描到的 token 键中提取 token 值(A3-12)
840    pub fn parse_token_from_key<'k>(
841        &self,
842        key: &'k str,
843        login_type: Option<&str>,
844    ) -> Option<&'k str> {
845        // ThreeSegment 默认体系走缓存前缀,避免每次分配。
846        // Default ThreeSegment uses the cached prefix to avoid allocation.
847        if matches!(self.layout, SaKeyLayout::ThreeSegment)
848            && login_type.map(Self::is_default_login_type).unwrap_or(true)
849        {
850            return key.strip_prefix(self.token_colon.as_ref());
851        }
852        let prefix = self.token_key_prefix(login_type);
853        key.strip_prefix(prefix.as_str())
854    }
855
856    /// Extracts the id segment from a scanned key of a given category (A3-12)
857    /// 从扫描到的指定分类键中提取 id 段(A3-12)
858    pub fn parse_id_from_key<'k>(
859        &self,
860        key: &'k str,
861        category: &str,
862        login_type: Option<&str>,
863    ) -> Option<&'k str> {
864        let prefix = self.category_prefix(category, login_type);
865        key.strip_prefix(prefix.as_str())
866    }
867}
868
869impl Default for SaKeys {
870    fn default() -> Self {
871        Self::new("sa:")
872    }
873}
874
875#[cfg(test)]
876mod tests {
877    use super::*;
878
879    fn legacy_make_key(prefix: &str, suffix: &str, id: &str) -> String {
880        format!("{prefix}{suffix}{id}")
881    }
882
883    fn id(s: &str) -> LoginId {
884        LoginId::new(s)
885    }
886
887    #[test]
888    fn account_ns_default_unchanged() {
889        assert_eq!(SaKeys::account_ns("default", &id("u1")).as_str(), "u1");
890        assert_eq!(SaKeys::account_ns("login", &id("u1")).as_str(), "u1");
891        assert_eq!(SaKeys::account_ns("", &id("u1")).as_str(), "u1");
892        assert_eq!(SaKeys::account_ns("admin", &id("u1")).as_str(), "admin:u1");
893    }
894
895    #[test]
896    fn account_ns_colon_escaping() {
897        assert_eq!(SaKeys::account_ns("default", &id("a:b")).as_str(), "a:b");
898        assert_eq!(
899            SaKeys::account_ns("admin", &id("a:b")).as_str(),
900            "admin:a%3Ab"
901        );
902        assert_ne!(
903            SaKeys::account_ns("a", &id("b:c")).as_str(),
904            SaKeys::account_ns("a:b", &id("c")).as_str()
905        );
906    }
907
908    #[test]
909    fn three_segment_matches_legacy_make_key() {
910        let keys = SaKeys::new("sa:");
911        let login_id = "user_1";
912        let token = "abc123";
913
914        assert_eq!(
915            keys.token_info(token),
916            legacy_make_key("sa:", "token:", token)
917        );
918        assert_eq!(
919            keys.login_token("default", login_id),
920            legacy_make_key("sa:", "login:token:", login_id)
921        );
922        assert_eq!(
923            keys.login_token_index("default", login_id),
924            legacy_make_key("sa:", "login:tokens:", login_id)
925        );
926        assert_eq!(
927            keys.account_session("default", login_id),
928            legacy_make_key("sa:", "session:", login_id)
929        );
930        assert_eq!(
931            keys.permission("default", login_id),
932            legacy_make_key("sa:", "permission:", login_id)
933        );
934        assert_eq!(
935            keys.role("default", login_id),
936            legacy_make_key("sa:", "role:", login_id)
937        );
938        assert_eq!(
939            keys.token_id_mapping(token),
940            legacy_make_key("sa:", "token-id:", token)
941        );
942        assert_eq!(
943            keys.token_session(token),
944            legacy_make_key("sa:", "token-session:", token)
945        );
946        assert_eq!(
947            keys.disable("default", login_id, "login"),
948            legacy_make_key("sa:", "disable:", &format!("{login_id}:login"))
949        );
950        assert_eq!(
951            keys.safe(token, "pay"),
952            legacy_make_key("sa:", "safe:", &format!("{token}:pay"))
953        );
954        assert_eq!(
955            keys.nonce("nonce_1"),
956            legacy_make_key("sa:", "nonce:", "nonce_1")
957        );
958        assert_eq!(
959            keys.refresh("rt_1"),
960            legacy_make_key("sa:", "refresh:", "rt_1")
961        );
962        assert_eq!(
963            keys.refresh_user_index("default", login_id),
964            legacy_make_key("sa:", "refresh:user:", login_id)
965        );
966    }
967
968    #[test]
969    fn three_segment_admin_account_keys() {
970        let keys = SaKeys::new("sa:");
971        assert_eq!(
972            keys.login_token("admin", "10001"),
973            "sa:login:token:admin:10001"
974        );
975        assert_eq!(
976            keys.login_token_index("admin", "10001"),
977            "sa:login:tokens:admin:10001"
978        );
979        assert_eq!(
980            keys.account_session("admin", "10001"),
981            "sa:session:admin:10001"
982        );
983    }
984
985    #[test]
986    fn session_by_ns_three_segment() {
987        let keys = SaKeys::new("sa:");
988        let ns = SaKeys::account_ns("admin", &id("10001"));
989        assert_eq!(keys.session_by_ns(&ns).unwrap(), "sa:session:admin:10001");
990    }
991
992    #[test]
993    fn custom_prefix_matches_legacy_make_key() {
994        let keys = SaKeys::new("myapp:");
995        assert_eq!(keys.token_info("t1"), "myapp:token:t1");
996        assert_eq!(keys.login_token("default", "u1"), "myapp:login:token:u1");
997    }
998
999    #[test]
1000    fn java_four_segment_layout() {
1001        let keys = SaKeys::with_layout("satoken", SaKeyLayout::JavaFourSegment);
1002        assert_eq!(keys.token_info("abc"), "satoken:login:token:abc");
1003        assert_eq!(
1004            keys.token_info_with_type("admin", "abc"),
1005            "satoken:admin:token:abc"
1006        );
1007        assert_eq!(
1008            keys.login_token("admin", "u1"),
1009            "satoken:admin:login:token:u1"
1010        );
1011        assert_eq!(
1012            keys.account_session("admin", "u1"),
1013            "satoken:admin:session:u1"
1014        );
1015        assert_eq!(
1016            keys.disable("admin", "u1", "login"),
1017            "satoken:admin:disable:login:u1"
1018        );
1019        assert_eq!(keys.safe("tok", "pay"), "satoken:login:safe:pay:tok");
1020    }
1021
1022    #[test]
1023    fn scan_and_parse_token() {
1024        let keys = SaKeys::new("sa:");
1025        assert_eq!(keys.token_scan_pattern(None), "sa:token:*");
1026        assert_eq!(
1027            keys.parse_token_from_key("sa:token:abc123", None),
1028            Some("abc123")
1029        );
1030        assert_eq!(keys.parse_token_from_key("sa:session:u1", None), None);
1031
1032        let keys = SaKeys::with_layout("satoken", SaKeyLayout::JavaFourSegment);
1033        assert_eq!(
1034            keys.token_scan_pattern(Some("admin")),
1035            "satoken:admin:token:*"
1036        );
1037        assert_eq!(
1038            keys.parse_token_from_key("satoken:admin:token:xyz", Some("admin")),
1039            Some("xyz")
1040        );
1041    }
1042
1043    #[test]
1044    fn from_config_uses_storage_prefix() {
1045        let config = SaTokenConfig::builder()
1046            .storage_key_prefix("app:")
1047            .build_config();
1048        let keys = SaKeys::from_config(&config);
1049        assert_eq!(keys.token_info("x"), "app:token:x");
1050    }
1051}