Skip to main content

sz_rust_core/middleware/
auth.rs

1//! Auth 中间件 — JWT 校验 + 白名单跳过(对齐 PHP `addons\BaseController`)
2//!
3//! 对齐 PHP `addons\BaseController.php` 的鉴权流程:
4//!
5//! ```php
6//! public function initialize() {
7//!     $this->getRouteinfo();          // 解析当前路由
8//!     $this->user = $this->getToken(); // JWT 校验
9//!     $this->checkLogin();             // 验证登录状态
10//! }
11//!
12//! public function getToken() {
13//!     if (!$token = Token::getUserId(request()->header('Authorization'))) {
14//!         if (in_array($this->routeUri, $this->allowAllAction)) {
15//!             return true;             // 白名单放行
16//!         } else {
17//!             throw new BaseException(['msg' => '缺少必要的参数,请重新登陆!']);
18//!         }
19//!     }
20//!     return $token;
21//! }
22//!
23//! private function checkLogin(): void {
24//!     if (in_array($this->routeUri, $this->allowAllAction)) {
25//!         return;                      // 白名单放行
26//!     }
27//!     if (!empty($this->user) && $this->user['is_login'] == 1) {
28//!         return;
29//!     }
30//!     throw new BaseException(['code' => -1, 'msg' => 'not_login']);
31//! }
32//! ```
33//!
34//! ## PHP 端默认白名单
35//!
36//! ```php
37//! protected array $allowAllAction = [
38//!     '/passport/login',
39//!     '/task/task/userClerk',
40//! ];
41//! ```
42//!
43//! ## PHP JWT 配置(`app\common\service\jwt\Token`)
44//!
45//! - 签发人:`https://mall.ljclz.shop`
46//! - 接收人:`https://mall.ljclz.shop`
47//! - 密钥:通过 `SZ_JWT_SECRET` 环境变量提供(P1-SEC-09:不再硬编码)
48//! - 有效期:30 天(`3600 * 24 * 30` 秒)
49//! - 算法:HS256
50//! - 自定义 claim:`user_id`
51//!
52//! PHP `Token::getUserId` 验证流程:
53//! 1. 从 `Authorization` header 取 token(去除 `bearer` 前缀,大小写不敏感)
54//! 2. 检查 token 是否在 `cache('delete_token')` 注销列表中(注销逻辑,缓存层实现)
55//! 3. 解析 JWT
56//! 4. 验证签发人(`IssuedBy`)
57//! 5. 验证接收人(`PermittedFor`)
58//! 6. 验证过期(`LooseValidAt`,时区 `Asia/Shanghai`)
59//! 7. 取出 `user_id`
60//!
61//! ## Rust 端实现说明
62//!
63//! 复用 `sz-orm-auth` 的 `JwtEncoder::decode` 进行签名 + 过期校验,
64//! 并在中间件层补充 PHP 端的额外校验:
65//!
66//! | PHP 验证项 | sz-orm-auth | Rust 中间件层补充 |
67//! |-----------|-------------|------------------|
68//! | 签名 | ✅ `JwtEncoder::decode` | — |
69//! | 过期 | ✅ `JwtEncoder::decode` | — |
70//! | 算法(HS256) | ✅ `JwtEncoder::decode` | — |
71//! | 签发人(`iss`) | ❌ 不校验 | ✅ 本模块补充 |
72//! | 接收人(`aud`) | ❌ 不校验 | ⚠️ 延迟到后续(Rust JwtClaims 无 `aud` 字段) |
73//! | `bearer` 前缀去除 | ❌ | ✅ 本模块补充 |
74//! | 白名单跳过 | ❌ | ✅ 本模块补充 |
75//! | 注销列表 | ❌ | ⚠️ 延迟到缓存层 |
76//!
77//! ## 错误码对齐
78//!
79//! | 场景 | PHP code | PHP msg | Rust ErrorCode |
80//! |------|---------|---------|---------------|
81//! | 缺少 Authorization header + 非白名单 | -1 | `缺少必要的参数,请重新登陆!` | `NotLogin` |
82//! | JWT 解析/校验失败 + 非白名单 | -1 | `缺少必要的参数,请重新登陆!` | `NotLogin` |
83//! | user_id 缺失/无效 + 非白名单 | -1 | `not_login` | `NotLogin` |
84//! | 白名单路由 | — | — | 放行(不校验) |
85//!
86//! PHP 端 `getToken()` 失败和 `checkLogin()` 失败都使用 `code = -1`,
87//! Rust 端统一使用 `ErrorCode::NotLogin`(`-1`)。
88
89use axum::extract::Request;
90use axum::http::StatusCode;
91use axum::middleware::Next;
92use axum::response::{IntoResponse, Response};
93use crate::orm::jwt::{JwtClaims, JwtEncoder};
94
95use crate::error::{BaseException, ErrorCode};
96
97/// PHP 端默认白名单(对齐 `addons\BaseController::$allowAllAction`)
98///
99/// ```php
100/// protected array $allowAllAction = [
101///     '/passport/login',
102///     '/task/task/userClerk',
103/// ];
104/// ```
105pub const DEFAULT_ALLOW_ALL_ACTION: &[&str] = &["/passport/login", "/task/task/userClerk"];
106
107/// PHP JWT 默认签发人(对齐 `app\common\service\jwt\Token::$_config['issuer']`)
108///
109/// **注意**:仅用于测试与文档对照。生产环境必须通过 `SZ_JWT_ISSUER` 环境变量覆盖。
110pub const DEFAULT_ISSUER: &str = "https://mall.ljclz.shop";
111
112/// PHP JWT 默认密钥(对齐 `app\common\service\jwt\Token::$_config['sign']`)。
113///
114/// **安全警告**:此常量保留 PHP 原始值仅供**测试与文档对照**使用。
115/// 生产环境必须通过 `SZ_JWT_SECRET` 环境变量提供密钥。
116/// `AuthConfig::default()` 会优先从 `SZ_JWT_SECRET` 环境变量读取;
117/// 仅在 `cfg(test)` 下回退到此常量以保持测试兼容性。
118///
119/// ## P1-SEC-09 修复说明
120///
121/// 旧版在 `cfg(test)` 下硬编码了 PHP JWT 密钥 `"shengzhuang"`,
122/// 该字符串会被编译进所有测试二进制文件。若测试二进制泄漏,JWT 密钥即已知。
123/// 修复方案:测试模式下改为运行时随机生成密钥(OsRng),不再包含任何硬编码秘密。
124#[cfg(test)]
125pub fn default_secret() -> String {
126    use rand::RngCore;
127    let mut bytes = [0u8; 32];
128    rand::rngs::OsRng.fill_bytes(&mut bytes);
129    // 生成 64 字符十六进制随机密钥,每次测试运行唯一
130    bytes.iter().map(|b| format!("{:02x}", b)).collect()
131}
132
133/// 生产环境占位符(任何构建下均为此值,不携带实际密钥)
134///
135/// 测试模式下 `AuthConfig::default()` 通过 `default_secret()` 运行时生成随机密钥,
136/// 不使用此常量作为实际密钥。此常量仅用于文档和断言目的。
137pub const DEFAULT_SECRET: &str = "<must-set-SZ_JWT_SECRET-env>";
138
139/// PHP JWT 默认有效期(秒)(对齐 `app\common\service\jwt\Token::$_config['expire'] = 3600 * 24 * 30`)
140pub const DEFAULT_EXPIRATION: u64 = 3600 * 24 * 30;
141
142/// Auth 中间件配置
143///
144/// 对齐 PHP `addons\BaseController` + `app\common\service\jwt\Token` 的配置。
145///
146/// ## P1-SEC-12 安全说明
147///
148/// `Debug` 手动实现:`secret` 字段始终脱敏为 `"[REDACTED]"`,
149/// 防止 `{:?}` 格式化时将密钥泄漏到日志或 panic 信息中。
150#[derive(Clone)]
151pub struct AuthConfig {
152    /// JWT 密钥(PHP `Token::$_config['sign']`)
153    pub secret: String,
154    /// JWT 签发人(PHP `Token::$_config['issuer']`)
155    pub issuer: String,
156    /// JWT 有效期(秒)(PHP `Token::$_config['expire']`)
157    pub expiration: u64,
158    /// 白名单路由列表(PHP `BaseController::$allowAllAction`)
159    ///
160    /// 支持通配符 `*`(对齐 PHP `AuthService::$allowAllAction` 中的 `/upload.library/*`)
161    pub allow_all_action: Vec<String>,
162}
163
164impl std::fmt::Debug for AuthConfig {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        f.debug_struct("AuthConfig")
167            .field("secret", &"[REDACTED]")
168            .field("issuer", &self.issuer)
169            .field("expiration", &self.expiration)
170            .field("allow_all_action", &self.allow_all_action)
171            .finish()
172    }
173}
174
175impl Default for AuthConfig {
176    fn default() -> Self {
177        // 生产环境:优先从环境变量读取 JWT 密钥
178        // 测试环境:运行时随机生成密钥(P1-SEC-09:不再硬编码任何秘密值)
179        let secret = std::env::var("SZ_JWT_SECRET").unwrap_or_else(|_| {
180            #[cfg(test)]
181            {
182                default_secret()
183            }
184            #[cfg(not(test))]
185            {
186                panic!("SZ_JWT_SECRET 环境变量未设置 — 生产环境必须通过环境变量提供 JWT 密钥");
187            }
188        });
189        let issuer = std::env::var("SZ_JWT_ISSUER").unwrap_or_else(|_| DEFAULT_ISSUER.to_string());
190        Self {
191            secret,
192            issuer,
193            expiration: DEFAULT_EXPIRATION,
194            allow_all_action: DEFAULT_ALLOW_ALL_ACTION
195                .iter()
196                .map(|s| s.to_string())
197                .collect(),
198        }
199    }
200}
201
202impl AuthConfig {
203    /// 从环境变量构造配置(生产环境推荐用法)
204    ///
205    /// # Errors
206    /// 当 `SZ_JWT_SECRET` 环境变量未设置时返回错误
207    pub fn from_env() -> Result<Self, std::env::VarError> {
208        Ok(Self {
209            secret: std::env::var("SZ_JWT_SECRET")?,
210            issuer: std::env::var("SZ_JWT_ISSUER").unwrap_or_else(|_| DEFAULT_ISSUER.to_string()),
211            expiration: DEFAULT_EXPIRATION,
212            allow_all_action: DEFAULT_ALLOW_ALL_ACTION
213                .iter()
214                .map(|s| s.to_string())
215                .collect(),
216        })
217    }
218
219    /// 创建带自定义白名单的配置
220    pub fn with_allow_all_action(mut self, allow: Vec<String>) -> Self {
221        self.allow_all_action = allow;
222        self
223    }
224
225    /// 创建带自定义密钥的配置
226    pub fn with_secret(mut self, secret: impl Into<String>) -> Self {
227        self.secret = secret.into();
228        self
229    }
230
231    /// 创建带自定义签发人的配置
232    pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
233        self.issuer = issuer.into();
234        self
235    }
236}
237
238/// Auth 中间件 — 对齐 PHP `addons\BaseController::initialize` 流程
239///
240/// ## 校验流程
241///
242/// 1. **路由白名单检查**:当前路由在 `allow_all_action` 中(含通配符匹配)→ 放行
243/// 2. **取 Authorization header**:缺失 → `BaseException(['code' => -1, 'msg' => '缺少必要的参数,请重新登陆!'])`
244/// 3. **去除 `bearer` 前缀**(大小写不敏感,对齐 PHP `Token::getRequestToken`)
245/// 4. **JWT 校验**:`JwtEncoder::decode` + 签发人校验
246///    - 解析失败 → `BaseException(['code' => -1, 'msg' => '缺少必要的参数,请重新登陆!'])`
247///    - 过期 → `BaseException(['code' => -1, 'msg' => '缺少必要的参数,请重新登陆!'])`
248///    - 签发人不匹配 → `BaseException(['code' => -1, 'msg' => '缺少必要的参数,请重新登陆!'])`
249/// 5. **user_id 校验**:`claims.user_id` 为 `None` 或 `Some(0)` →
250///    `BaseException(['code' => -1, 'msg' => 'not_login'])`
251/// 6. **通过校验**:将 `user_id` 插入 request extensions,调用 `next`
252///
253/// ## 用法
254///
255/// ```ignore
256/// use sz_rust_core::middleware::auth::{auth_middleware, AuthConfig};
257/// use axum::Router;
258///
259/// let config = AuthConfig::default();
260/// let app: Router = Router::new()
261///     .route("/", axum::routing::get(|| async { "ok" }))
262///     .layer(axum::middleware::from_fn_with_state(config, auth_middleware));
263/// ```
264#[tracing::instrument(skip_all)]
265pub async fn auth_middleware(
266    axum::extract::State(config): axum::extract::State<AuthConfig>,
267    req: Request,
268    next: Next,
269) -> Response {
270    // 1. 路由白名单检查(对齐 PHP `checkLogin` 中的 `in_array($this->routeUri, $this->allowAllAction)`)
271    let route_uri = extract_route_uri(&req);
272    if is_route_allowed(&route_uri, &config.allow_all_action) {
273        return next.run(req).await.into_response();
274    }
275
276    // 2. 取 Authorization header(对齐 PHP `Token::getUserId(request()->header('Authorization'))`)
277    let auth_header = req.headers().get(axum::http::header::AUTHORIZATION);
278    let token = match auth_header {
279        Some(value) => {
280            let raw = value.to_str().unwrap_or("");
281            // 3. 去除 bearer 前缀(大小写不敏感,对齐 PHP `trim(str_ireplace('bearer', '', $header))`)
282            extract_token_from_header(raw)
283        }
284        None => None,
285    };
286
287    let token = match token {
288        Some(t) if !t.is_empty() => t,
289        _ => {
290            // PHP: `throw new BaseException(['msg' => '缺少必要的参数,请重新登陆!'])`
291            return base_exception_to_response(BaseException::not_login(
292                "缺少必要的参数,请重新登陆!",
293            ));
294        }
295    };
296
297    // 4. JWT 校验:JwtEncoder::decode + 签发人校验
298    let encoder = JwtEncoder::new(&config.secret);
299    let claims = match encoder.decode(&token) {
300        Ok(c) => c,
301        Err(_) => {
302            // PHP 端 `Token::getUserId` 在任何 JWT 校验失败时都返回 null,
303            // `BaseController::getToken` 把 null 当作「缺少必要的参数」处理
304            return base_exception_to_response(BaseException::not_login(
305                "缺少必要的参数,请重新登陆!",
306            ));
307        }
308    };
309
310    // 4.1 签发人校验(对齐 PHP `IssuedBy(self::$_config['issuer'])`)
311    if !verify_issuer(&claims, &config.issuer) {
312        return base_exception_to_response(BaseException::not_login("缺少必要的参数,请重新登陆!"));
313    }
314
315    // 5. user_id 校验(对齐 PHP `checkLogin` 中的 `$this->user['is_login'] == 1`)
316    let user_id = match claims.user_id {
317        Some(id) if id > 0 => id,
318        _ => {
319            // PHP: `throw new BaseException(['code' => -1, 'msg' => 'not_login'])`
320            return base_exception_to_response(BaseException::not_login("not_login"));
321        }
322    };
323
324    // 6. 通过校验:将 user_id 插入 request extensions,调用 next
325    let mut req = req;
326    req.extensions_mut().insert(AuthenticatedUser { user_id });
327    next.run(req).await.into_response()
328}
329
330/// 将 `BaseException` 转换为 HTTP 响应
331///
332/// 使用 `BaseException::code` 对应的 HTTP 状态码(通过 `ErrorCode::from(code).http_status()`),
333/// 响应体为标准 JSON 格式 `{"code":<code>,"msg":"<msg>","data":{}}`(对齐 PHP `renderJson`)。
334pub fn base_exception_to_response(exc: BaseException) -> Response {
335    let http_status = ErrorCode::from(exc.code).http_status();
336    let body = exc.to_json().to_string();
337    (
338        StatusCode::from_u16(http_status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
339        [(
340            axum::http::header::CONTENT_TYPE,
341            "application/json; charset=utf-8",
342        )],
343        body,
344    )
345        .into_response()
346}
347
348/// 已认证用户信息(插入 request extensions,供后续 handler 使用)
349#[derive(Debug, Clone, Copy)]
350pub struct AuthenticatedUser {
351    /// 用户 ID
352    pub user_id: i64,
353}
354
355/// 从 Authorization header 值中提取 token(去除 `bearer` 前缀)
356///
357/// 对齐 PHP `Token::getRequestToken`:
358/// ```php
359/// $method = 'bearer';
360/// return trim(str_ireplace($method, '', $header));
361/// ```
362///
363/// PHP 的 `str_ireplace` 是大小写不敏感的字符串替换,会替换所有出现位置。
364/// Rust 端去除前缀(`Bearer ` / `bearer` / `BEARER` 等),对齐 PHP 行为:
365/// - `Bearer xxx` → `xxx`
366/// - `bearer xxx` → `xxx`(大小写不敏感)
367/// - `bearerxxx` → `xxx`(对齐 PHP `str_ireplace` 无空格替换)
368/// - `xxx` → `xxx`(无 bearer 前缀时返回原值)
369pub fn extract_token_from_header(header: &str) -> Option<String> {
370    let trimmed = header.trim();
371    if trimmed.is_empty() {
372        return None;
373    }
374    // 大小写不敏感匹配 bearer 前缀(对齐 PHP `str_ireplace`)
375    // bearer 全部为 ASCII,使用 strip_prefix 后剩余部分用原始字符串截取以保留 token 大小写
376    let lower = trimmed.to_lowercase();
377    if let Some(suffix_len) = lower.strip_prefix("bearer ").map(|s| s.len()) {
378        // 去除前缀后剩余部分(用原始字符串截取以保留大小写)
379        let rest = &trimmed[trimmed.len() - suffix_len..];
380        Some(rest.trim().to_string())
381    } else if let Some(suffix_len) = lower.strip_prefix("bearer").map(|s| s.len()) {
382        // 处理 `bearerxxx`(无空格)的情况,对齐 PHP `str_ireplace` 行为
383        let rest = &trimmed[trimmed.len() - suffix_len..];
384        Some(rest.trim().to_string())
385    } else {
386        // 无 bearer 前缀,直接返回原值(对齐 PHP:`str_ireplace` 找不到时不替换)
387        Some(trimmed.to_string())
388    }
389}
390
391/// 提取请求的路由 URI(用于白名单匹配)
392///
393/// 对齐 PHP `BaseController::getRouteinfo`:
394/// ```php
395/// $this->routeUri = '/' . $this->controller . '/' . $this->action;
396/// ```
397///
398/// Rust 端使用 `req.uri().path()`,并去掉查询字符串。
399pub fn extract_route_uri(req: &Request) -> String {
400    req.uri().path().to_string()
401}
402
403/// 判断路由是否在白名单中(支持通配符 `*`)
404///
405/// 对齐 PHP `AuthService::$allowAllAction` 中的通配符匹配:
406/// - 精确匹配:`/passport/login` == `/passport/login` → true
407/// - 通配符匹配:`/upload.library/*` 匹配 `/upload.library/any` → true
408/// - 通配符匹配:`/upload.library/*` 匹配 `/upload.library/sub/deep` → true
409///   (PHP `fnmatch` 的 `*` 匹配任意字符包括 `/`)
410pub fn is_route_allowed(route_uri: &str, allow_list: &[String]) -> bool {
411    for pattern in allow_list {
412        if pattern == route_uri {
413            return true;
414        }
415        if pattern.contains('*') && wildcard_match(pattern, route_uri) {
416            return true;
417        }
418    }
419    false
420}
421
422/// 通配符匹配(`*` 匹配任意字符包括 `/`,对齐 PHP `fnmatch`)
423///
424/// 仅支持 `*` 通配符(对齐 PHP `AuthService` 白名单的实际使用场景)。
425/// 不支持 `?`、`[`、`]` 等 fnmatch 特殊字符(PHP `fnmatch` 支持,
426/// 但 PHP `AuthService` 的白名单中只用了 `*`)。
427pub fn wildcard_match(pattern: &str, text: &str) -> bool {
428    simple_wildcard_match(pattern, text)
429}
430
431/// 简单通配符匹配(仅支持 `*`,对齐 PHP `fnmatch` 中 `*` 的语义)
432///
433/// 算法:动态规划,时间复杂度 O(m*n)
434fn simple_wildcard_match(pattern: &str, text: &str) -> bool {
435    let p: Vec<char> = pattern.chars().collect();
436    let t: Vec<char> = text.chars().collect();
437    let m = p.len();
438    let n = t.len();
439
440    // dp[i][j] = pattern[0..i] 匹配 text[0..j]
441    let mut dp = vec![vec![false; n + 1]; m + 1];
442    dp[0][0] = true;
443
444    // pattern 以 * 开头时可以匹配空字符串
445    for i in 1..=m {
446        if p[i - 1] == '*' {
447            dp[i][0] = dp[i - 1][0];
448        }
449    }
450
451    for i in 1..=m {
452        for j in 1..=n {
453            if p[i - 1] == '*' {
454                // * 匹配 0 个字符(dp[i-1][j])或多个字符(dp[i][j-1])
455                dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
456            } else if p[i - 1] == t[j - 1] {
457                dp[i][j] = dp[i - 1][j - 1];
458            }
459        }
460    }
461
462    dp[m][n]
463}
464
465/// 校验 JWT 签发人(对齐 PHP `IssuedBy` constraint)
466///
467/// PHP 端使用 `Lcobucci\JWT\Validation\Constraint\IssuedBy`:
468/// ```php
469/// $issued = new IssuedBy(self::$_config['issuer']);
470/// if (!$config->validator()->validate($token, $issued)) {
471///     return null;
472/// }
473/// ```
474///
475/// Rust 端 `JwtEncoder::decode` 不校验签发人,需在本模块补充。
476#[tracing::instrument(skip(claims))]
477pub fn verify_issuer(claims: &JwtClaims, expected_issuer: &str) -> bool {
478    match &claims.iss {
479        Some(iss) => iss == expected_issuer,
480        None => false,
481    }
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487    use axum::body::Body;
488    use axum::http::StatusCode;
489    use axum::Router;
490    use http_body_util::BodyExt;
491    use tower::ServiceExt;
492
493    // ====================================================================
494    // 辅助函数
495    // ====================================================================
496
497    async fn read_body(resp: Response) -> String {
498        let bytes = resp.into_body().collect().await.unwrap().to_bytes();
499        String::from_utf8(bytes.to_vec()).unwrap()
500    }
501
502    fn make_request_with_uri(method: &str, uri: &str) -> Request {
503        Request::builder()
504            .method(method)
505            .uri(uri)
506            .body(Body::empty())
507            .unwrap()
508    }
509
510    fn make_request_with_auth(method: &str, uri: &str, auth: &str) -> Request {
511        Request::builder()
512            .method(method)
513            .uri(uri)
514            .header("Authorization", auth)
515            .body(Body::empty())
516            .unwrap()
517    }
518
519    /// 生成有效 JWT token(用于测试)
520    fn make_test_token(secret: &str, issuer: &str, user_id: i64, exp_offset_secs: i64) -> String {
521        let encoder = JwtEncoder::new(secret);
522        let now = std::time::SystemTime::now()
523            .duration_since(std::time::UNIX_EPOCH)
524            .unwrap()
525            .as_secs() as i64;
526        let claims = JwtClaims::new("test_user", now + exp_offset_secs)
527            .with_issuer(issuer)
528            .with_user_id(user_id);
529        encoder.encode(&claims).expect("encode token")
530    }
531
532    // ====================================================================
533    // extract_token_from_header 单元测试
534    // ====================================================================
535
536    #[test]
537    fn test_extract_token_from_header_with_bearer_prefix() {
538        // 对齐 PHP: `Bearer xxx` → `xxx`
539        let token = extract_token_from_header("Bearer abc123");
540        assert_eq!(token, Some("abc123".to_string()));
541    }
542
543    #[test]
544    fn test_extract_token_from_header_with_lowercase_bearer() {
545        // 对齐 PHP: `bearer xxx` → `xxx`(大小写不敏感)
546        let token = extract_token_from_header("bearer abc123");
547        assert_eq!(token, Some("abc123".to_string()));
548    }
549
550    #[test]
551    fn test_extract_token_from_header_with_uppercase_bearer() {
552        // 对齐 PHP: `BEARER xxx` → `xxx`(大小写不敏感)
553        let token = extract_token_from_header("BEARER abc123");
554        assert_eq!(token, Some("abc123".to_string()));
555    }
556
557    #[test]
558    fn test_extract_token_from_header_without_bearer_prefix() {
559        // 对齐 PHP: 无 bearer 前缀时直接返回原值
560        let token = extract_token_from_header("abc123");
561        assert_eq!(token, Some("abc123".to_string()));
562    }
563
564    #[test]
565    fn test_extract_token_from_header_with_empty_string() {
566        let token = extract_token_from_header("");
567        assert_eq!(token, None);
568    }
569
570    #[test]
571    fn test_extract_token_from_header_with_only_whitespace() {
572        let token = extract_token_from_header("   ");
573        assert_eq!(token, None);
574    }
575
576    #[test]
577    fn test_extract_token_from_header_with_bearer_no_space() {
578        // 对齐 PHP `str_ireplace('bearer', '', 'bearerabc')` → `abc`
579        let token = extract_token_from_header("bearerabc");
580        assert_eq!(token, Some("abc".to_string()));
581    }
582
583    #[test]
584    fn test_extract_token_from_header_trims_whitespace() {
585        // 对齐 PHP `trim(...)` 去除首尾空白
586        let token = extract_token_from_header("  Bearer   abc123  ");
587        assert_eq!(token, Some("abc123".to_string()));
588    }
589
590    // ====================================================================
591    // is_route_allowed / wildcard_match 单元测试
592    // ====================================================================
593
594    #[test]
595    fn test_is_route_allowed_exact_match() {
596        let allow = vec!["/passport/login".to_string()];
597        assert!(is_route_allowed("/passport/login", &allow));
598        assert!(!is_route_allowed("/passport/logout", &allow));
599    }
600
601    #[test]
602    fn test_is_route_allowed_multiple_entries() {
603        let allow = vec![
604            "/passport/login".to_string(),
605            "/task/task/userClerk".to_string(),
606        ];
607        assert!(is_route_allowed("/passport/login", &allow));
608        assert!(is_route_allowed("/task/task/userClerk", &allow));
609        assert!(!is_route_allowed("/passport/logout", &allow));
610    }
611
612    #[test]
613    fn test_is_route_allowed_wildcard_suffix() {
614        // 对齐 PHP `AuthService::$allowAllAction` 中的 `/upload.library/*`
615        let allow = vec!["/upload.library/*".to_string()];
616        assert!(is_route_allowed("/upload.library/any", &allow));
617        assert!(is_route_allowed("/upload.library/sub/deep", &allow));
618        assert!(!is_route_allowed("/upload.library", &allow)); // 缺少分隔
619        assert!(!is_route_allowed("/other/path", &allow));
620    }
621
622    #[test]
623    fn test_is_route_allowed_empty_list() {
624        let allow: Vec<String> = vec![];
625        assert!(!is_route_allowed("/any/path", &allow));
626    }
627
628    #[test]
629    fn test_wildcard_match_plain() {
630        assert!(wildcard_match("/upload/*", "/upload/any"));
631        assert!(wildcard_match("/upload/*", "/upload/sub/deep"));
632        assert!(!wildcard_match("/upload/*", "/other/any"));
633    }
634
635    #[test]
636    fn test_wildcard_match_exact_no_star() {
637        // 无 * 时退化为精确匹配
638        assert!(wildcard_match("/passport/login", "/passport/login"));
639        assert!(!wildcard_match("/passport/login", "/passport/logout"));
640    }
641
642    #[test]
643    fn test_wildcard_match_multiple_stars() {
644        assert!(wildcard_match("/*/*", "/a/b"));
645        assert!(wildcard_match("/*/*", "/abc/def"));
646        assert!(!wildcard_match("/*/*", "/a"));
647    }
648
649    #[test]
650    fn test_wildcard_match_star_at_end() {
651        assert!(wildcard_match("/api/*", "/api/v1/users"));
652        assert!(wildcard_match("/api/*", "/api/"));
653        assert!(!wildcard_match("/api/*", "/api"));
654    }
655
656    #[test]
657    fn test_wildcard_match_empty_pattern_and_text() {
658        assert!(wildcard_match("", ""));
659        assert!(!wildcard_match("", "abc"));
660        assert!(!wildcard_match("abc", ""));
661    }
662
663    #[test]
664    fn test_wildcard_match_star_only() {
665        // `*` 匹配任意字符串(包括空)
666        assert!(wildcard_match("*", ""));
667        assert!(wildcard_match("*", "anything"));
668        assert!(wildcard_match("*", "/path/to/anything"));
669    }
670
671    // ====================================================================
672    // verify_issuer 单元测试
673    // ====================================================================
674
675    #[test]
676    fn test_verify_issuer_matches() {
677        let claims = JwtClaims::new("user", 9999999999).with_issuer("https://mall.ljclz.shop");
678        assert!(verify_issuer(&claims, "https://mall.ljclz.shop"));
679    }
680
681    #[test]
682    fn test_verify_issuer_mismatch() {
683        let claims = JwtClaims::new("user", 9999999999).with_issuer("https://evil.com");
684        assert!(!verify_issuer(&claims, "https://mall.ljclz.shop"));
685    }
686
687    #[test]
688    fn test_verify_issuer_missing() {
689        // 无签发人时校验失败(对齐 PHP `IssuedBy` 约束失败)
690        let claims = JwtClaims::new("user", 9999999999);
691        assert!(!verify_issuer(&claims, "https://mall.ljclz.shop"));
692    }
693
694    // ====================================================================
695    // AuthConfig 单元测试
696    // ====================================================================
697
698    #[test]
699    fn test_auth_config_default_matches_php() {
700        // 对齐 PHP `Token::$_config` 默认值
701        let config = AuthConfig::default();
702        // P1-SEC-09: 测试模式下 secret 为运行时随机生成(不再硬编码 "shengzhuang")
703        // 验证其长度符合 OsRng 32 字节 → 64 hex 字符的预期
704        assert_eq!(config.secret.len(), 64, "测试模式 secret 应为 64 字符随机密钥");
705        assert_eq!(config.issuer, "https://mall.ljclz.shop");
706        assert_eq!(config.expiration, 3600 * 24 * 30);
707        // 对齐 PHP `BaseController::$allowAllAction`
708        assert_eq!(
709            config.allow_all_action,
710            vec![
711                "/passport/login".to_string(),
712                "/task/task/userClerk".to_string(),
713            ]
714        );
715    }
716
717    #[test]
718    fn test_auth_config_default_allow_all_action_constant() {
719        // 验证常量与 Default 一致
720        assert_eq!(DEFAULT_ALLOW_ALL_ACTION.len(), 2);
721        assert_eq!(DEFAULT_ALLOW_ALL_ACTION[0], "/passport/login");
722        assert_eq!(DEFAULT_ALLOW_ALL_ACTION[1], "/task/task/userClerk");
723    }
724
725    #[test]
726    fn test_auth_config_builder_methods() {
727        let config = AuthConfig::default()
728            .with_secret("custom-secret")
729            .with_issuer("https://custom.com")
730            .with_allow_all_action(vec!["/custom/login".to_string()]);
731
732        assert_eq!(config.secret, "custom-secret");
733        assert_eq!(config.issuer, "https://custom.com");
734        assert_eq!(config.allow_all_action, vec!["/custom/login".to_string()]);
735    }
736
737    #[test]
738    fn test_auth_default_constants_match_php() {
739        // 对齐 PHP `Token::$_config` 常量
740        assert_eq!(DEFAULT_ISSUER, "https://mall.ljclz.shop");
741        // P1-SEC-09: 生产占位符不再是实际密钥值
742        assert_eq!(DEFAULT_SECRET, "<must-set-SZ_JWT_SECRET-env>");
743        assert_eq!(DEFAULT_EXPIRATION, 3600 * 24 * 30);
744    }
745
746    // ====================================================================
747    // extract_route_uri 单元测试
748    // ====================================================================
749
750    #[test]
751    fn test_extract_route_uri_strips_query_string() {
752        let req = Request::builder()
753            .uri("/passport/login?foo=bar&baz=qux")
754            .body(Body::empty())
755            .unwrap();
756        assert_eq!(extract_route_uri(&req), "/passport/login");
757    }
758
759    #[test]
760    fn test_extract_route_uri_no_query() {
761        let req = Request::builder()
762            .uri("/api/users")
763            .body(Body::empty())
764            .unwrap();
765        assert_eq!(extract_route_uri(&req), "/api/users");
766    }
767
768    #[test]
769    fn test_extract_route_uri_root() {
770        let req = Request::builder().uri("/").body(Body::empty()).unwrap();
771        assert_eq!(extract_route_uri(&req), "/");
772    }
773
774    // ====================================================================
775    // auth_middleware 集成测试(通过 Router 验证)
776    // ====================================================================
777
778    /// 构建测试用 Router,使用给定 AuthConfig
779    fn build_app(config: AuthConfig) -> Router {
780        Router::new()
781            .route("/protected", axum::routing::get(|| async { "protected" }))
782            .route("/passport/login", axum::routing::get(|| async { "login" }))
783            .route(
784                "/upload.library/test",
785                axum::routing::get(|| async { "upload" }),
786            )
787            .layer(axum::middleware::from_fn_with_state(
788                config,
789                auth_middleware,
790            ))
791    }
792
793    #[tokio::test]
794    async fn test_auth_middleware_allows_whitelisted_route() {
795        // 对齐 PHP: 白名单 `/passport/login` 跳过 Auth 校验
796        let app = build_app(AuthConfig::default());
797        let resp = app
798            .oneshot(make_request_with_uri("GET", "/passport/login"))
799            .await
800            .unwrap();
801        assert_eq!(resp.status(), StatusCode::OK);
802        let body = read_body(resp).await;
803        assert_eq!(body, "login");
804    }
805
806    #[tokio::test]
807    async fn test_auth_middleware_rejects_missing_authorization_header() {
808        // 对齐 PHP: 无 Authorization header → BaseException(['msg' => '缺少必要的参数,请重新登陆!'])
809        let app = build_app(AuthConfig::default());
810        let resp = app
811            .oneshot(make_request_with_uri("GET", "/protected"))
812            .await
813            .unwrap();
814        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); // NotLogin → 401
815        let body = read_body(resp).await;
816        assert!(body.contains("\"code\":-1"));
817        assert!(body.contains("缺少必要的参数,请重新登陆!"));
818    }
819
820    #[tokio::test]
821    async fn test_auth_middleware_rejects_empty_authorization_header() {
822        // 对齐 PHP: Authorization header 为空字符串
823        let app = build_app(AuthConfig::default());
824        let resp = app
825            .oneshot(make_request_with_auth("GET", "/protected", ""))
826            .await
827            .unwrap();
828        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
829        let body = read_body(resp).await;
830        assert!(body.contains("\"code\":-1"));
831    }
832
833    #[tokio::test]
834    async fn test_auth_middleware_rejects_invalid_token() {
835        // 对齐 PHP: JWT 解析失败 → BaseException(['msg' => '缺少必要的参数,请重新登陆!'])
836        let app = build_app(AuthConfig::default());
837        let resp = app
838            .oneshot(make_request_with_auth(
839                "GET",
840                "/protected",
841                "invalid.token.here",
842            ))
843            .await
844            .unwrap();
845        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
846        let body = read_body(resp).await;
847        assert!(body.contains("\"code\":-1"));
848        assert!(body.contains("缺少必要的参数,请重新登陆!"));
849    }
850
851    #[tokio::test]
852    async fn test_auth_middleware_rejects_expired_token() {
853        // 对齐 PHP: token 过期 → BaseException(['msg' => '缺少必要的参数,请重新登陆!'])
854        let config = AuthConfig::default();
855        // 生成已过期的 token(exp = now - 3600)
856        let token = make_test_token(&config.secret, &config.issuer, 1, -3600);
857        let app = build_app(config.clone());
858        let resp = app
859            .oneshot(make_request_with_auth("GET", "/protected", &token))
860            .await
861            .unwrap();
862        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
863        let body = read_body(resp).await;
864        assert!(body.contains("\"code\":-1"));
865    }
866
867    #[tokio::test]
868    async fn test_auth_middleware_rejects_wrong_secret_token() {
869        // 对齐 PHP: 用错误密钥签发的 token 无法通过签名校验
870        let config = AuthConfig::default();
871        let token = make_test_token("wrong-secret", &config.issuer, 1, 3600);
872        let app = build_app(config.clone());
873        let resp = app
874            .oneshot(make_request_with_auth("GET", "/protected", &token))
875            .await
876            .unwrap();
877        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
878    }
879
880    #[tokio::test]
881    async fn test_auth_middleware_rejects_wrong_issuer_token() {
882        // 对齐 PHP: 签发人不匹配 → IssuedBy 约束失败
883        let config = AuthConfig::default();
884        let token = make_test_token(&config.secret, "https://evil.com", 1, 3600);
885        let app = build_app(config.clone());
886        let resp = app
887            .oneshot(make_request_with_auth("GET", "/protected", &token))
888            .await
889            .unwrap();
890        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
891        let body = read_body(resp).await;
892        assert!(body.contains("缺少必要的参数,请重新登陆!"));
893    }
894
895    #[tokio::test]
896    async fn test_auth_middleware_rejects_token_without_user_id() {
897        // 对齐 PHP: user_id 缺失 → BaseException(['code' => -1, 'msg' => 'not_login'])
898        let config = AuthConfig::default();
899        let encoder = JwtEncoder::new(&config.secret);
900        let now = std::time::SystemTime::now()
901            .duration_since(std::time::UNIX_EPOCH)
902            .unwrap()
903            .as_secs() as i64;
904        // 不设置 user_id
905        let claims = JwtClaims::new("test_user", now + 3600).with_issuer(&config.issuer);
906        let token = encoder.encode(&claims).unwrap();
907        let app = build_app(config.clone());
908        let resp = app
909            .oneshot(make_request_with_auth("GET", "/protected", &token))
910            .await
911            .unwrap();
912        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
913        let body = read_body(resp).await;
914        assert!(body.contains("\"code\":-1"));
915        assert!(body.contains("not_login"));
916    }
917
918    #[tokio::test]
919    async fn test_auth_middleware_rejects_token_with_zero_user_id() {
920        // user_id = 0 视为无效(对齐 PHP `$this->user['is_login'] == 1` 检查)
921        let config = AuthConfig::default();
922        let token = make_test_token(&config.secret, &config.issuer, 0, 3600);
923        let app = build_app(config.clone());
924        let resp = app
925            .oneshot(make_request_with_auth("GET", "/protected", &token))
926            .await
927            .unwrap();
928        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
929        let body = read_body(resp).await;
930        assert!(body.contains("not_login"));
931    }
932
933    #[tokio::test]
934    async fn test_auth_middleware_accepts_valid_token_with_bearer_prefix() {
935        // 对齐 PHP: `Bearer <token>` 通过校验
936        let config = AuthConfig::default();
937        let token = make_test_token(&config.secret, &config.issuer, 42, 3600);
938        let app = build_app(config.clone());
939        let resp = app
940            .oneshot(make_request_with_auth(
941                "GET",
942                "/protected",
943                &format!("Bearer {}", token),
944            ))
945            .await
946            .unwrap();
947        assert_eq!(resp.status(), StatusCode::OK);
948        let body = read_body(resp).await;
949        assert_eq!(body, "protected");
950    }
951
952    #[tokio::test]
953    async fn test_auth_middleware_accepts_valid_token_without_bearer_prefix() {
954        // 对齐 PHP: `str_ireplace` 找不到 bearer 时直接返回原 token
955        let config = AuthConfig::default();
956        let token = make_test_token(&config.secret, &config.issuer, 42, 3600);
957        let app = build_app(config.clone());
958        let resp = app
959            .oneshot(make_request_with_auth("GET", "/protected", &token))
960            .await
961            .unwrap();
962        assert_eq!(resp.status(), StatusCode::OK);
963    }
964
965    #[tokio::test]
966    async fn test_auth_middleware_accepts_lowercase_bearer_prefix() {
967        // 对齐 PHP: `bearer <token>` 大小写不敏感
968        let config = AuthConfig::default();
969        let token = make_test_token(&config.secret, &config.issuer, 42, 3600);
970        let app = build_app(config.clone());
971        let resp = app
972            .oneshot(make_request_with_auth(
973                "GET",
974                "/protected",
975                &format!("bearer {}", token),
976            ))
977            .await
978            .unwrap();
979        assert_eq!(resp.status(), StatusCode::OK);
980    }
981
982    #[tokio::test]
983    async fn test_auth_middleware_supports_wildcard_whitelist() {
984        // 对齐 PHP `AuthService::$allowAllAction` 中的 `/upload.library/*`
985        let config =
986            AuthConfig::default().with_allow_all_action(vec!["/upload.library/*".to_string()]);
987        let app = build_app(config);
988        let resp = app
989            .oneshot(make_request_with_uri("GET", "/upload.library/test"))
990            .await
991            .unwrap();
992        assert_eq!(resp.status(), StatusCode::OK);
993        let body = read_body(resp).await;
994        assert_eq!(body, "upload");
995    }
996
997    #[tokio::test]
998    async fn test_auth_middleware_wildcard_does_not_overmatch() {
999        // 通配符 `/upload.library/*` 不应匹配 `/upload.library`(无尾部路径)
1000        let config =
1001            AuthConfig::default().with_allow_all_action(vec!["/upload.library/*".to_string()]);
1002        let app = build_app(config);
1003        // `/upload.library/test` 在白名单中,但 `/protected` 不在
1004        let resp = app
1005            .oneshot(make_request_with_uri("GET", "/protected"))
1006            .await
1007            .unwrap();
1008        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1009    }
1010
1011    #[tokio::test]
1012    async fn test_auth_middleware_injects_user_id_into_extensions() {
1013        // 验证通过校验后,user_id 被插入 request extensions
1014        let config = AuthConfig::default();
1015        let token = make_test_token(&config.secret, &config.issuer, 99, 3600);
1016        let app = Router::new()
1017            .route(
1018                "/protected",
1019                axum::routing::get(|req: Request| async move {
1020                    let user = req.extensions().get::<AuthenticatedUser>().unwrap();
1021                    format!("user_id:{}", user.user_id)
1022                }),
1023            )
1024            .layer(axum::middleware::from_fn_with_state(
1025                config.clone(),
1026                auth_middleware,
1027            ));
1028        let resp = app
1029            .oneshot(make_request_with_auth("GET", "/protected", &token))
1030            .await
1031            .unwrap();
1032        assert_eq!(resp.status(), StatusCode::OK);
1033        let body = read_body(resp).await;
1034        assert_eq!(body, "user_id:99");
1035    }
1036
1037    #[tokio::test]
1038    async fn test_auth_middleware_returns_correct_error_code_for_missing_token() {
1039        // 验证错误码对齐 PHP `BaseException(['code' => -1, ...])`
1040        let app = build_app(AuthConfig::default());
1041        let resp = app
1042            .oneshot(make_request_with_uri("GET", "/protected"))
1043            .await
1044            .unwrap();
1045        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); // ErrorCode::NotLogin → 401
1046        let body = read_body(resp).await;
1047        // JSON 响应格式:{"code":-1,"msg":"...","data":{}}
1048        let json: serde_json::Value = serde_json::from_str(&body).unwrap();
1049        assert_eq!(json["code"], -1);
1050        assert_eq!(json["msg"], "缺少必要的参数,请重新登陆!");
1051        assert_eq!(json["data"], serde_json::json!({}));
1052    }
1053
1054    #[tokio::test]
1055    async fn test_auth_middleware_returns_correct_error_code_for_not_login() {
1056        // 验证 user_id 缺失时错误码为 -1,msg 为 "not_login"
1057        let config = AuthConfig::default();
1058        let encoder = JwtEncoder::new(&config.secret);
1059        let now = std::time::SystemTime::now()
1060            .duration_since(std::time::UNIX_EPOCH)
1061            .unwrap()
1062            .as_secs() as i64;
1063        let claims = JwtClaims::new("test_user", now + 3600).with_issuer(&config.issuer);
1064        let token = encoder.encode(&claims).unwrap();
1065        let app = build_app(config.clone());
1066        let resp = app
1067            .oneshot(make_request_with_auth("GET", "/protected", &token))
1068            .await
1069            .unwrap();
1070        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1071        let body = read_body(resp).await;
1072        let json: serde_json::Value = serde_json::from_str(&body).unwrap();
1073        assert_eq!(json["code"], -1);
1074        assert_eq!(json["msg"], "not_login");
1075    }
1076
1077    #[tokio::test]
1078    async fn test_auth_middleware_custom_secret_and_issuer() {
1079        // 验证自定义密钥和签发人配置生效
1080        let config = AuthConfig::default()
1081            .with_secret("custom-secret")
1082            .with_issuer("https://custom.com");
1083        let token = make_test_token("custom-secret", "https://custom.com", 1, 3600);
1084        let app = build_app(config);
1085        let resp = app
1086            .oneshot(make_request_with_auth("GET", "/protected", &token))
1087            .await
1088            .unwrap();
1089        assert_eq!(resp.status(), StatusCode::OK);
1090    }
1091
1092    #[tokio::test]
1093    async fn test_auth_middleware_rejects_token_signed_with_default_secret_when_custom_configured()
1094    {
1095        // 自定义密钥后,用不同密钥签发的 token 应被拒绝
1096        let config = AuthConfig::default().with_secret("custom-secret");
1097        let token = make_test_token("wrong-secret", &config.issuer, 1, 3600);
1098        let app = build_app(config);
1099        let resp = app
1100            .oneshot(make_request_with_auth("GET", "/protected", &token))
1101            .await
1102            .unwrap();
1103        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1104    }
1105
1106    #[tokio::test]
1107    async fn test_auth_middleware_preserves_query_string_in_route_match() {
1108        // 验证带查询参数的白名单路由仍能匹配
1109        let app = build_app(AuthConfig::default());
1110        let resp = app
1111            .oneshot(make_request_with_uri(
1112                "GET",
1113                "/passport/login?redirect=/home",
1114            ))
1115            .await
1116            .unwrap();
1117        assert_eq!(resp.status(), StatusCode::OK);
1118    }
1119
1120    #[tokio::test]
1121    async fn test_auth_middleware_handles_token_with_only_bearer_prefix() {
1122        // `Bearer ` 后无内容 → 视为空 token
1123        let app = build_app(AuthConfig::default());
1124        let resp = app
1125            .oneshot(make_request_with_auth("GET", "/protected", "Bearer "))
1126            .await
1127            .unwrap();
1128        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1129    }
1130
1131    // ====================================================================
1132    // HeaderMap 与 PHP 对齐验证
1133    // ====================================================================
1134
1135    #[test]
1136    fn test_authorization_header_name_aligns_with_php() {
1137        // PHP: `request()->header('Authorization')`
1138        // Rust: `req.headers().get(axum::http::header::AUTHORIZATION)`
1139        // 两者都使用标准 HTTP header 名 `Authorization`
1140        let header_name = axum::http::header::AUTHORIZATION;
1141        assert_eq!(header_name.as_str(), "authorization");
1142        // axum/http 的 header 名是 lowercase,PHP 端不区分大小写
1143    }
1144
1145    // ====================================================================
1146    // PHP 行为对齐验证测试
1147    // ====================================================================
1148
1149    #[test]
1150    fn test_php_default_allow_all_action_matches_rust() {
1151        // PHP `BaseController::$allowAllAction` 默认值
1152        let php_allow = vec!["/passport/login", "/task/task/userClerk"];
1153        // Rust `DEFAULT_ALLOW_ALL_ACTION`
1154        assert_eq!(php_allow, DEFAULT_ALLOW_ALL_ACTION);
1155    }
1156
1157    #[test]
1158    fn test_php_jwt_config_matches_rust() {
1159        // PHP `Token::$_config`
1160        let php_issuer = "https://mall.ljclz.shop";
1161        let php_expire = 3600 * 24 * 30;
1162
1163        // Rust 默认常量
1164        assert_eq!(php_issuer, DEFAULT_ISSUER);
1165        assert_eq!(php_expire, DEFAULT_EXPIRATION);
1166        // P1-SEC-09: DEFAULT_SECRET 在生产构建中为占位符,不再是硬编码密钥
1167        assert_eq!(DEFAULT_SECRET, "<must-set-SZ_JWT_SECRET-env>");
1168    }
1169}