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}