Skip to main content

tdm_server_rust/utils/
jwt.rs

1//! JWT 工具 (JSON Web Token Utilities)
2//!
3//! 基于 `jsonwebtoken` crate 的 Token 生成与解析。
4//! 对齐 Java `JwtUtils`。
5
6use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// JWT 负载
11#[derive(Debug, Serialize, Deserialize, Clone)]
12pub struct JwtClaims {
13    /// 组员 ID
14    pub id: i32,
15    /// 用户名(可选)
16    #[serde(default)]
17    pub username: Option<String>,
18    /// 实习状态(可选)
19    #[serde(default)]
20    pub intern: Option<i16>,
21    /// 岗位 ID 列表(可选)
22    #[serde(default)]
23    pub posts: Option<Vec<i32>>,
24    /// 过期时间戳
25    pub exp: i64,
26}
27
28/// JWT 生成与解析工具
29///
30/// 基于 HMAC-SHA256 签名的 JWT Token 管理。
31///
32/// # 示例
33///
34/// ```
35/// use tdm_server_rust::utils::JwtUtil;
36///
37/// let util = JwtUtil::new("my_secret_key", 7_200_000); // 2h 过期
38/// let token = util.generate(42).expect("生成应成功");
39///
40/// // 解析并验证
41/// let claims = util.parse(&token).expect("解析应成功");
42/// assert_eq!(claims.id, 42);
43/// ```
44///
45/// # Panics
46///
47/// 不 panic。所有错误通过 `jsonwebtoken::errors::Result` 返回。
48///
49/// # 安全性
50///
51/// - 签名密钥从配置文件 `jwt.sign_key` 加载
52/// - 默认有效期由 `jwt.expire_ms` 控制
53/// - 解析时自动验证签名和过期时间
54pub struct JwtUtil {
55    /// 签名密钥
56    sign_key: String,
57    /// 过期毫秒
58    expire_ms: i64,
59}
60
61impl JwtUtil {
62    /// 构造 JWT 工具实例。
63    ///
64    /// # 参数
65    ///
66    /// - `sign_key`: HMAC-SHA256 签名密钥
67    /// - `expire_ms`: Token 过期时间(毫秒)
68    ///
69    /// # 返回值
70    ///
71    /// 返回配置好的 [`JwtUtil`]。
72    pub fn new(sign_key: impl Into<String>, expire_ms: i64) -> Self {
73        Self {
74            sign_key: sign_key.into(),
75            expire_ms,
76        }
77    }
78
79    /// 生成 JWT Token。
80    ///
81    /// Claims 仅包含 `id` 和 `exp` 字段,与 Java 端一致。
82    ///
83    /// # 参数
84    ///
85    /// - `member_id`: 组员 ID
86    ///
87    /// # 返回值
88    ///
89    /// 成功返回签名的 JWT 字符串。
90    ///
91    /// # Errors
92    ///
93    /// 仅当 HMAC 签名算法不可用时失败(不应发生)。
94    #[tracing::instrument(skip(self), level = "debug")]
95    pub fn generate(&self, member_id: i32) -> jsonwebtoken::errors::Result<String> {
96        let exp = chrono::Utc::now().timestamp_millis() + self.expire_ms;
97        let claims = JwtClaims {
98            id: member_id,
99            username: None,
100            intern: None,
101            posts: None,
102            exp: exp / 1000,
103        };
104        encode(
105            &Header::default(),
106            &claims,
107            &EncodingKey::from_secret(self.sign_key.as_bytes()),
108        )
109    }
110
111    /// 解析并验证 JWT Token。
112    ///
113    /// 自动验证签名和过期时间。
114    ///
115    /// # 参数
116    ///
117    /// - `token`: 待解析的 JWT 字符串
118    ///
119    /// # 返回值
120    ///
121    /// 成功返回反序列化的 [`JwtClaims`]。
122    ///
123    /// # Errors
124    ///
125    /// | 错误 | 说明 |
126    /// |------|------|
127    /// | `InvalidSignature` | 签名不匹配(密钥错误或 token 被篡改) |
128    /// | `ExpiredSignature` | Token 已过期 |
129    /// | `InvalidToken` | Token 格式不合法 |
130    ///
131    /// 在 auth 中间件中,`ExpiredSignature` 会被转为 `AppError::login_expired("登录数据过期,请重新登录喵!")`。
132    #[tracing::instrument(skip(self, token), level = "debug")]
133    pub fn parse(&self, token: &str) -> jsonwebtoken::errors::Result<JwtClaims> {
134        let data = decode::<JwtClaims>(
135            token,
136            &DecodingKey::from_secret(self.sign_key.as_bytes()),
137            &Validation::default(),
138        )?;
139        Ok(data.claims)
140    }
141
142    /// 从 HashMap 生成(扩展用)
143    pub fn generate_from_map(&self, claims: HashMap<String, serde_json::Value>) -> jsonwebtoken::errors::Result<String> {
144        let id = claims.get("id").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
145        self.generate(id)
146    }
147}