认证(JWT)
大约 5 分钟
JWT 认证系统
JSON Web Token (JWT) 是我们应用中实现 无状态身份认证 的核心技术。本文档将深入解析我们 JWT 系统的设计、工作流程、安全特性以及如何在应用中使用它。
我们的系统主要由两部分构成:
- Token 生成: 在用户成功登录后,为其签发一个有时效性的、包含用户身份信息的 JWT。
- Token 验证: 在每个受保护的 API 请求中,验证其携带的 JWT 是否合法有效,并从中解析出用户信息。
JWT 结构与 Claims
一个 JWT 由三部分组成:Header (头部)、Payload (载荷)、Signature (签名)。我们关注的核心是 Payload,它承载了我们自定义的数据,由 Claims
结构体定义:
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
/// 用户唯一 ID (User ID)
pub uid: i64,
/// Token 唯一 ID,用于白名单验证
pub token_id: i64,
/// 用户名
pub username: String,
/// 过期时间戳 (Expiration Time)
pub exp: i64,
}
uid
和username
: 标识了 Token 的所有者。exp
: 标准的 JWT 字段,定义了 Token 的过期时间。一旦当前时间超过exp
,Token 将自动失效。token_id
: 这是我们系统的一个关键安全增强。它是一个与数据库中sys_white_jwt
表记录相关联的唯一 ID,用于实现 Token 的主动失效(登出)功能。
工作流程
1. Token 生成 (authorize
函数)
当用户通过用户名和密码成功登录后,系统会调用 authorize
函数来为他生成一个新的 Token。
流程如下:
- 接收
AuthPayload
: 包含用户的核心信息 (uid
,username
等)。 - 生成
token_id
: 在调用authorize
之前,系统会向sys_white_jwt
表中插入一条新记录,并获得一个自增的token_id
。这条记录代表了“一次有效的登录会话”。 - 设置过期时间: 计算出当前时间戳加上配置的有效期(
APPCOFIG.auth.jwt.expiration
),得到exp
。 - 创建
Claims
: 将用户信息、token_id
和exp
封装成Claims
实例。 - 签名 (Sign): 使用全局唯一的密钥 (
KEYS.encoding
) 对 Header 和Claims
进行签名,生成最终的 JWT 字符串。 - 返回
AuthBody
: 将生成的 Token 和过期时间等信息包装成AuthBody
,作为登录接口的响应返回给客户端。
2. Token 验证 (UserInfo
提取器)
Token 验证是 自动发生 在所有受保护的 API 请求中的。这是通过为 UserInfo
实现 FromRequestParts
Trait(使其成为一个 Axum 提取器)来完成的。
当一个 Handler 的参数中包含 user_info: UserInfo
时,Axum 会自动执行以下验证流程:
- 提取 Bearer Token: 从请求头的
Authorization: Bearer <token>
中解析出 Token 字符串。如果不存在或格式不正确,直接拒绝请求。 - 解码与基础验证: 使用公钥 (
KEYS.decoding
) 解码 Token。jsonwebtoken
库会自动完成两项基础验证:- 签名验证: 确保 Token 没有被篡改。
- 过期验证: 确保
exp
字段表示的时间尚未过去。 - 如果任一验证失败,请求将被拒绝并返回相应的
401 Unauthorized
错误。
- 白名单验证 (核心安全特性):
- 从解码后的
Claims
中获取token_id
。 - 调用
s_sys_white_jwt::get_token(token_id)
查询数据库(或缓存)。 - 检查该
token_id
是否依然存在于白名单表中。 - 如果记录 不存在(例如,因为用户执行了“登出”操作,该记录已被删除),即使 Token 本身未过期,验证也会失败,返回
AuthError::CheckOutToken
(“账户已登出”)。 - 这有效地实现了 Token 的主动失效/吊销功能。
- 从解码后的
- 获取最新用户信息:
- 使用
Claims
中的uid
调用s_sys_user::get_user_info
查询数据库,获取用户最新的信息(特别是角色 IDrid
和设备 IDdid
)。 - 这确保了即使用户的角色在 Token 有效期内被管理员更改,后续的 API 权限检查也能立即使用最新的角色信息。
- 使用
- 注入
UserInfo
:- 将所有验证通过后的信息组装成一个完整的
UserInfo
实例。 - 通过
parts.extensions.insert(user.clone())
将UserInfo
注入到请求扩展中,供后续的中间件(如权限检查)和 Handler 使用。 - 将
UserInfo
作为提取器的结果返回。
- 将所有验证通过后的信息组装成一个完整的
安全设计考量
点击展开查看我们 JWT 系统的安全特性
无状态与有状态的平衡:
- 传统的 JWT 是完全无状态的,一旦签发,在过期前无法撤销。
- 我们的系统通过引入
token_id
和白名单表,实现了一种 “半有状态” 模式。它保留了 JWT 无需每次请求都查用户密码的性能优势,同时又获得了能够 随时让任意 Token 失效 的强大安全控制能力。
防范角色变更延迟:
- 普通 JWT 系统的一个风险是:如果用户的权限被降级,但其手中的旧 Token 尚未过期,他依然能凭借旧 Token 访问他已无权访问的接口。
- 我们的系统通过在每次请求时都重新查询用户的最新信息 (
s_sys_user::get_user_info
),完美地解决了这个问题,确保了权限变更的实时性。
中心化密钥管理:
- 所有 Token 的签名和验证都使用
once_cell::sync::Lazy
初始化的全局静态KEYS
。密钥从配置文件中读取,保证了整个应用中密钥的一致性和安全性。
- 所有 Token 的签名和验证都使用
清晰的错误类型 (
AuthError
):AuthError
枚举定义了所有可能发生的认证失败场景(如InvalidToken
,ExpiredToken
,CheckOutToken
),并为每种场景映射了精确的 HTTP 状态码和对用户友好的错误消息。
总结
我们的 JWT 认证系统不仅提供了标准的 Token 生成和验证功能,更通过 白名单机制 和 实时用户信息查询 两个关键设计,极大地增强了系统的安全性和可控性,为构建一个安全可靠的应用奠定了坚实的基础。