Skip to main content

sa_token_core/
router.rs

1// Author: 金书记
2//
3// Path-based authentication router module
4// 基于路径的鉴权路由模块
5
6use std::sync::Arc;
7
8use sa_token_adapter::context::SaRequest;
9
10use crate::config::SaTokenConfig;
11use crate::token_io;
12
13type LoginIdValidator = Arc<dyn Fn(&str) -> bool + Send + Sync>;
14
15/// Match a path against a pattern (Ant-style wildcard)
16/// 匹配路径与模式(Ant 风格通配符)
17///
18/// # Arguments
19/// - `path`: The request path to match
20/// - `pattern`: The pattern to match against
21///
22/// # Patterns Supported
23/// - `/**`: Match all paths
24/// - `/api/**`: Match all paths starting with `/api/`
25/// - `/api/*`: Match single-level paths under `/api/`
26/// - `*.html`: Match paths ending with `.html`
27/// - `/exact`: Exact match
28///
29/// # Examples
30/// ```
31/// use sa_token_core::router::match_path;
32/// assert!(match_path("/api/user", "/api/**"));
33/// assert!(match_path("/api/user", "/api/*"));
34/// assert!(!match_path("/api/user/profile", "/api/*"));
35/// ```
36pub fn match_path(path: &str, pattern: &str) -> bool {
37    if pattern == "/**" {
38        return true;
39    }
40    if let Some(prefix) = pattern.strip_suffix("/**") {
41        return path.starts_with(prefix);
42    }
43    if let Some(suffix) = pattern.strip_prefix("*") {
44        return path.ends_with(suffix);
45    }
46    // `/*`: single path segment after prefix (e.g. `/api/*` matches `/api/user`, not `/api/a/b`).
47    // `/*`:前缀后仅一层路径段(如 `/api/*` 匹配 `/api/user`,不匹配 `/api/a/b`)。
48    if let Some(prefix) = pattern.strip_suffix("/*") {
49        if !path.starts_with(prefix) {
50            return false;
51        }
52        let rest = &path[prefix.len()..];
53        if rest.is_empty() || rest == "/" {
54            return true;
55        }
56        let rest = rest.trim_start_matches('/');
57        return !rest.contains('/');
58    }
59    path == pattern
60}
61
62/// Check if path matches any pattern in the list
63/// 检查路径是否匹配列表中的任意模式
64pub fn match_any(path: &str, patterns: &[&str]) -> bool {
65    patterns.iter().any(|p| match_path(path, p))
66}
67
68/// Determine if authentication is needed for a path
69/// 判断路径是否需要鉴权
70///
71/// Returns `true` if path matches include patterns but not exclude patterns
72/// 如果路径匹配包含模式但不匹配排除模式,返回 `true`
73pub fn need_auth(path: &str, include: &[&str], exclude: &[&str]) -> bool {
74    match_any(path, include) && !match_any(path, exclude)
75}
76
77/// Path-based authentication configuration
78/// 基于路径的鉴权配置
79///
80/// Configure which paths require authentication and which are excluded
81/// 配置哪些路径需要鉴权,哪些路径被排除
82#[derive(Clone)]
83pub struct PathAuthConfig {
84    /// Paths that require authentication (include patterns)
85    /// 需要鉴权的路径(包含模式)
86    include: Vec<String>,
87    /// Paths excluded from authentication (exclude patterns)
88    /// 排除鉴权的路径(排除模式)
89    exclude: Vec<String>,
90    /// Optional login ID validator function
91    /// 可选的登录ID验证函数
92    validator: Option<LoginIdValidator>,
93}
94
95impl std::fmt::Debug for PathAuthConfig {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        f.write_str("PathAuthConfig { .. }")
98    }
99}
100
101impl PathAuthConfig {
102    /// Create a new path authentication configuration
103    /// 创建新的路径鉴权配置
104    pub fn new() -> Self {
105        Self {
106            include: Vec::new(),
107            exclude: Vec::new(),
108            validator: None,
109        }
110    }
111
112    /// Set paths that require authentication
113    /// 设置需要鉴权的路径
114    pub fn include(mut self, patterns: Vec<String>) -> Self {
115        self.include = patterns;
116        self
117    }
118
119    /// Set paths excluded from authentication
120    /// 设置排除鉴权的路径
121    pub fn exclude(mut self, patterns: Vec<String>) -> Self {
122        self.exclude = patterns;
123        self
124    }
125
126    /// Set a custom login ID validator function
127    /// 设置自定义的登录ID验证函数
128    pub fn validator<F>(mut self, f: F) -> Self
129    where
130        F: Fn(&str) -> bool + Send + Sync + 'static,
131    {
132        self.validator = Some(Arc::new(f));
133        self
134    }
135
136    /// Check if a path requires authentication
137    /// 检查路径是否需要鉴权
138    pub fn check(&self, path: &str) -> bool {
139        let inc: Vec<&str> = self.include.iter().map(|s| s.as_str()).collect();
140        let exc: Vec<&str> = self.exclude.iter().map(|s| s.as_str()).collect();
141        need_auth(path, &inc, &exc)
142    }
143
144    /// Validate a login ID using the configured validator
145    /// 使用配置的验证器验证登录ID
146    pub fn validate_login_id(&self, login_id: &str) -> bool {
147        self.validator.as_ref().is_none_or(|v| v(login_id))
148    }
149}
150
151impl Default for PathAuthConfig {
152    fn default() -> Self {
153        Self::new()
154    }
155}
156
157use crate::context::{RequestAuthMeta, SaTokenContext};
158use crate::{SaTokenManager, TokenValue, token::TokenInfo};
159
160/// Authentication result after processing
161/// 处理后的鉴权结果
162pub struct AuthResult {
163    /// Whether authentication is required for this path
164    /// 此路径是否需要鉴权
165    pub need_auth: bool,
166    /// Extracted token value
167    /// 提取的token值
168    pub token: Option<TokenValue>,
169    /// Token information if valid
170    /// 如果有效则包含token信息
171    pub token_info: Option<TokenInfo>,
172    /// Whether the token is valid
173    /// token是否有效
174    pub is_valid: bool,
175}
176
177impl std::fmt::Debug for AuthResult {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        f.write_str("AuthResult { .. }")
180    }
181}
182
183impl AuthResult {
184    /// Check if the request should be rejected
185    /// 检查请求是否应该被拒绝
186    pub fn should_reject(&self) -> bool {
187        self.need_auth && (!self.is_valid || self.token.is_none())
188    }
189
190    /// Get the login ID from token info
191    /// 从token信息中获取登录ID
192    pub fn login_id(&self) -> Option<&str> {
193        self.token_info.as_ref().map(|t| t.login_id.as_ref())
194    }
195}
196
197/// Process authentication for a request path
198/// 处理请求路径的鉴权
199///
200/// This function checks if the path requires authentication, validates the token,
201/// and returns an AuthResult with all relevant information.
202/// 此函数检查路径是否需要鉴权,验证token,并返回包含所有相关信息的AuthResult。
203///
204/// # Arguments
205/// - `path`: The request path
206/// - `token_str`: Optional token string from request
207/// - `config`: Path authentication configuration
208/// - `manager`: SaTokenManager instance
209pub async fn process_auth(
210    path: &str,
211    token_str: Option<String>,
212    config: &PathAuthConfig,
213    manager: &SaTokenManager,
214) -> AuthResult {
215    let need_auth = config.check(path);
216
217    let token = token_str.map(TokenValue::new);
218
219    let (is_valid, token_info) = if let Some(ref t) = token {
220        let valid = manager.is_valid(t).await;
221        let info = if valid {
222            manager.get_token_info(t).await.ok()
223        } else {
224            None
225        };
226        (valid, info)
227    } else {
228        (false, None)
229    };
230
231    let is_valid = is_valid
232        && if need_auth {
233            token_info
234                .as_ref()
235                .is_some_and(|info| config.validate_login_id(info.login_id.as_ref()))
236        } else {
237            true
238        };
239
240    AuthResult {
241        need_auth,
242        token,
243        token_info,
244        is_valid,
245    }
246}
247
248/// 从鉴权结果创建 SaTokenContext(共享 Arc Inner,供 scope 内 switch_to 突变)
249///
250/// Create `SaTokenContext` from auth result (shared Arc Inner for `switch_to` mutations inside scope).
251pub fn create_context(result: &AuthResult, auth_meta: RequestAuthMeta) -> SaTokenContext {
252    let mut builder = SaTokenContext::builder().auth_meta(auth_meta);
253    if let (Some(token), Some(info)) = (&result.token, &result.token_info) {
254        builder = builder
255            .token(token.clone())
256            .token_info(Arc::new(info.clone()))
257            .login_id(info.login_id.as_ref());
258    }
259    builder.build()
260}
261
262/// Backward-compatible extract: all read flags true, no custom prefix.
263/// 兼容旧签名:读取开关全开、无自定义前缀。
264pub fn extract_token<R: SaRequest>(req: &R, token_name: &str) -> Option<String> {
265    let cfg = SaTokenConfig {
266        token_name: token_name.to_string(),
267        ..SaTokenConfig::default()
268    };
269    token_io::read_token(req, &cfg)
270}
271
272/// Extract using the live manager config (`is_read_*` / `token_prefix`).
273/// 使用当前 Manager 配置抽取(尊重 `is_read_*` / `token_prefix`)。
274pub fn extract_token_from<R: SaRequest>(req: &R, config: &SaTokenConfig) -> Option<String> {
275    token_io::read_token(req, config)
276}
277
278/// Outcome of [`run_auth_flow`]; bindings copy token/login_id/context into framework-specific storage (extensions, depot, etc.).
279/// [`run_auth_flow`] 的返回结果;各框架绑定把 token / login_id / context 写入自身存储(extensions、Depot 等)。
280pub struct AuthFlowResult {
281    /// Path rules + validation summary. | 路径规则与校验摘要。
282    pub auth: AuthResult,
283    /// Login id when token is valid. | 登录 id(token 有效时)。
284    pub login_id: Option<String>,
285    /// Parsed token value when present. | 解析后的 token(若有)。
286    pub token: Option<TokenValue>,
287    /// Request-scoped context for `StpUtil` / handlers. | 请求级上下文,供 `StpUtil` / 处理器使用。
288    pub context: SaTokenContext,
289}
290
291impl std::fmt::Debug for AuthFlowResult {
292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293        f.write_str("AuthFlowResult { .. }")
294    }
295}
296
297impl AuthFlowResult {
298    /// `true` if the binding should respond **401** (path requires auth but token missing or invalid).
299    /// 若路径要求鉴权但 token 缺失或无效,绑定层应返回 **401**,则返回 `true`。
300    pub fn should_reject(&self) -> bool {
301        self.auth.should_reject()
302    }
303
304    /// Run `fut` with [`SaTokenContext::scope`] using this flow's [`AuthFlowResult::context`] (await-safe).
305    /// 用本流的 [`AuthFlowResult::context`] 调用 [`SaTokenContext::scope`] 执行 `fut`(可跨 await)。
306    ///
307    /// Context 内部可变:scope 后 `switch_to` 可就地突变共享 Arc。
308    /// 同时建立授权快照:B2 特性,请求级权限缓存。
309    ///
310    /// Context is internally mutable: `switch_to` can mutate shared Arc in-place after scope.
311    /// Also establishes authz snapshot (B2 feature, request-level permission cache).
312    pub async fn run<F, R>(self, fut: F) -> R
313    where
314        F: Future<Output = R>,
315    {
316        SaTokenContext::scope(self.context, fut).await
317    }
318}
319
320/// Full auth pipeline: [`extract_token_from`] → optional [`PathAuthConfig`] via [`process_auth`], else default check → [`create_context`].
321/// 完整鉴权流水线:[`extract_token_from`] → 若有 [`PathAuthConfig`] 则 [`process_auth`],否则默认校验 → [`create_context`]。
322///
323/// Pass `path_config: None` for “validate token if present, no path-based reject”.
324/// `path_config` 为 `None` 时表示:有 token 则校验并填上下文,不按路径规则拒绝。
325pub async fn run_auth_flow<R: SaRequest>(
326    req: &R,
327    manager: &SaTokenManager,
328    path_config: Option<&PathAuthConfig>,
329) -> AuthFlowResult {
330    let token_str = extract_token_from(req, &manager.config);
331    let path = req.get_path();
332    let auth_meta = RequestAuthMeta::from_request(req, manager.config.same_token_header.as_str());
333
334    let (auth, ctx) = match path_config {
335        Some(cfg) => {
336            // Path-based rules: may set need_auth / should_reject.
337            // 基于路径的规则:可产生 need_auth / should_reject。
338            let auth = process_auth(path.as_str(), token_str.clone(), cfg, manager).await;
339            let ctx = create_context(&auth, auth_meta);
340            (auth, ctx)
341        }
342        None => {
343            // No path config: only validate token when present.
344            // 无路径配置:仅在有 token 时做有效性校验。
345            let token = token_str.map(TokenValue::new);
346            let (is_valid, token_info) = if let Some(ref t) = token {
347                let valid = manager.is_valid(t).await;
348                let info = if valid {
349                    manager.get_token_info(t).await.ok()
350                } else {
351                    None
352                };
353                (valid, info)
354            } else {
355                (false, None)
356            };
357            let auth = AuthResult {
358                need_auth: false,
359                token: token.clone(),
360                token_info,
361                is_valid,
362            };
363            let ctx = create_context(&auth, auth_meta);
364            (auth, ctx)
365        }
366    };
367
368    let login_id = auth.login_id().map(str::to_string);
369    let token = auth.token.clone();
370    AuthFlowResult {
371        auth,
372        login_id,
373        token,
374        context: ctx,
375    }
376}