从零实现基于 Axum 的 JWT 认证系统
本教程介绍了如何在Rust的Axum框架中实现JWT认证系统。主要内容包括:定义JWT的数据结构(Claims、AuthPayload等)、实现认证错误处理、创建令牌生成与验证功能、设置受保护路由,以及测试系统功能。通过jsonwebtoken库处理令牌的编码/解码,使用LazyLock管理全局密钥,并实现FromRequestParts来自动验证请求中的JWT令牌。教程还展示了如何通过curl命
·
JWT(JSON Web Token)是一种用于在网络应用间安全传递信息的紧凑、自包含的方式。本教程将带你逐步实现一个基于 Axum 框架的 JWT 认证系统,包括令牌生成、验证和受保护路由访问。
准备工作
首先,我们需要创建一个新的 Rust 项目并添加必要的依赖:
cargo new axum-jwt-demo
cd axum-jwt-demo
在Cargo.toml
中添加依赖:
[package]
name = "jwt_demo"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = "0.8.4"
axum-extra = { version = "0.10.1", features = ["typed-header"] }
jsonwebtoken = "9.3.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.143"
tokio = { version = "1.47.1", features = ["full"] }
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
第一步:理解 JWT 基本结构
JWT 由三部分组成,用点分隔:
- 头部 (Header):指定令牌类型和加密算法
- 载荷 (Claims):包含要传递的信息(如用户 ID、过期时间等)
- 签名 (Signature):使用密钥对前两部分进行签名,确保信息未被篡改
第二步:定义核心数据结构
创建src/main.rs
,首先定义我们需要的数据结构:
use serde::{Deserialize, Serialize};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
use std::sync::LazyLock;
// JWT声明结构体 - 包含需要传递的信息
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String, // 主题(通常是用户ID)
company: String, // 自定义字段:公司名称
exp: usize, // 过期时间(Unix时间戳)
}
// 认证请求体 - 客户端发送的凭据
#[derive(Debug, Deserialize)]
struct AuthPayload {
client_id: String, // 客户端ID
client_secret: String // 客户端密钥
}
// 认证响应体 - 返回给客户端的令牌信息
#[derive(Debug, Serialize)]
struct AuthBody {
access_token: String, // 访问令牌
token_type: String, // 令牌类型(通常为"Bearer")
}
// JWT密钥管理结构体
struct Keys {
encoding: EncodingKey, // 用于生成令牌的编码密钥
decoding: DecodingKey // 用于验证令牌的解码密钥
}
impl Keys {
fn new(secret: &[u8]) -> Self {
Self {
encoding: EncodingKey::from_secret(secret),
decoding: DecodingKey::from_secret(secret)
}
}
}
// 全局密钥实例 - 程序启动时初始化
static KEYS: LazyLock<Keys> = LazyLock::new(|| {
let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET必须设置");
Keys::new(secret.as_bytes())
});
第三步:实现认证错误处理
定义认证过程中可能出现的错误类型,并实现 Axum 的响应转换:
use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
use serde_json::json;
// 认证相关错误枚举
#[derive(Debug)]
enum AuthError {
WrongCredentials, // 凭据错误
MissingCredentials, // 缺少凭据
TokenCreation, // 令牌创建失败
InvalidToken // 无效令牌
}
// 实现错误到HTTP响应的转换
impl IntoResponse for AuthError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
AuthError::WrongCredentials => (StatusCode::UNAUTHORIZED, "凭据错误"),
AuthError::MissingCredentials => (StatusCode::BAD_REQUEST, "缺少凭据"),
AuthError::TokenCreation => (StatusCode::INTERNAL_SERVER_ERROR, "令牌创建失败"),
AuthError::InvalidToken => (StatusCode::BAD_REQUEST, "无效的令牌"),
};
let body = Json(json!({
"error": error_message
}));
(status, body).into_response()
}
}
第四步:实现令牌生成功能
创建处理客户端认证请求并生成 JWT 令牌的处理函数:
use axum::Json;
// 处理认证请求,生成JWT令牌
async fn authorize(Json(payload): Json<AuthPayload>) -> Result<Json<AuthBody>, AuthError> {
// 检查是否提供了必要的凭据
if payload.client_id.is_empty() || payload.client_secret.is_empty() {
return Err(AuthError::MissingCredentials);
}
// 验证凭据(实际应用中应从数据库查询)
// 这里使用硬编码的"foo"和"bar"作为有效凭据
if payload.client_id != "foo" || payload.client_secret != "bar" {
return Err(AuthError::WrongCredentials);
}
// 创建JWT声明
let claims = Claims {
sub: "user@example.com".to_owned(), // 用户标识
company: "ACME".to_owned(), // 自定义信息
exp: 2000000000, // 过期时间(2033年)
};
// 生成JWT令牌
let token = jsonwebtoken::encode(&Header::default(), &claims, &KEYS.encoding)
.map_err(|_| AuthError::TokenCreation)?;
// 返回包含令牌的响应
Ok(Json(AuthBody {
access_token: token,
token_type: "Bearer".to_string()
}))
}
// 为AuthBody实现辅助方法
impl AuthBody {
fn new(access_token: String) -> Self {
Self {
access_token,
token_type: "Bearer".to_string()
}
}
}
第五步:实现令牌验证功能
实现从请求中提取并验证 JWT 令牌的功能,使 Axum 能够自动验证受保护路由:
use axum::{extract::FromRequestParts, http::request::Parts};
use axum_extra::{headers::{authorization::Bearer, Authorization}, TypedHeader};
// 实现从请求中提取Claims的逻辑
#[axum::async_trait]
impl<S> FromRequestParts<S> for Claims
where
S: Send + Sync,
{
type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
// 从请求头中提取Authorization: Bearer <token>
let TypedHeader(Authorization(bearer)) = parts
.extract::<TypedHeader<Authorization<Bearer>>>()
.await
.map_err(|_| AuthError::InvalidToken)?;
// 验证并解码JWT令牌
let token_data = jsonwebtoken::decode::<Claims>(
bearer.token(),
&KEYS.decoding,
&Validation::default()
).map_err(|_| AuthError::InvalidToken)?;
Ok(token_data.claims)
}
}
第六步:创建受保护路由
实现一个需要有效 JWT 令牌才能访问的受保护路由:
// 受保护的路由处理函数
async fn protected(claims: Claims) -> Result<String, AuthError> {
Ok(format!(
"欢迎访问受保护区域!\n您的信息:\n用户: {}\n公司: {}",
claims.sub, claims.company
))
}
// 为Claims实现Display trait,方便格式化输出
impl std::fmt::Display for Claims {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "用户: {}\n公司: {}", self.sub, self.company)
}
}
第七步:设置路由和启动服务器
配置 Axum 路由并启动服务器:
use axum::{routing::{get, post}, Router};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main]
async fn main() {
// 初始化日志系统
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "axum_jwt_demo=debug".into()))
.with(tracing_subscriber::fmt::layer())
.init();
// 创建路由
let app = Router::new()
.route("/protected", get(protected)) // 受保护路由
.route("/authorize", post(authorize)); // 认证路由
// 绑定地址并启动服务器
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
println!("服务器运行在 http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
第八步:测试系统功能
现在我们的 JWT 认证系统已经完成,让's 测试它的功能:
- 首先设置环境变量并启动服务器:
set JWT_SECRET=ABCDEFGHIJKLMN
cargo run
- 获取 JWT 令牌:
curl -X POST http://localhost:3000/authorize -H "Content-Type: application/json" -d "{\"client_id\":\"foo\",\"client_secret\":\"bar\"}"
执行结果:
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjIwMDAwMDAwMDB9.79nkwbx5eHI4GNa5xw0eBEsKhkr0dNFncqms3txhFj0","token_type":"Bearer"}
- 使用令牌访问受保护路由(将下面的
<token>
替换为上一步获取的令牌):
curl http://localhost:3000/protected -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29tIiwiY29tcGFueSI6IkFDTUUiLCJleHAiOjIwMDAwMDAwMDB9.79nkwbx5eHI4GNa5xw0eBEsKhkr0dNFncqms3txhFj0"
执行结果:
Welcome to the protected area :)
Your data:
Email: b@b.com
Company: ACME
- 尝试使用无效令牌访问(应该返回错误):
curl http://localhost:3000/protected -H "Authorization: Bearer asdfasdfasdff"
执行结果:
{"error":"Invalid token"}
总结与扩展
通过本教程,我们实现了一个完整的 JWT 认证系统,包括:
- JWT 令牌的生成与验证
- 基于 Axum 的路由和请求处理
- 错误处理和 HTTP 响应转换
- 受保护资源的访问控制
在实际应用中,你可能需要:
- 从数据库验证用户凭据
- 实现更短的令牌过期时间并添加刷新令牌机制
- 增加更多的 JWT 声明字段
- 添加 HTTPS 以确保传输安全
- 实现更细粒度的权限控制
这个基础框架可以根据你的具体需求进行扩展,为你的 Web 应用提供安全可靠的身份验证机制。
附录:全部代码
//! JWT 授权(authorization) / 认证示例(authentication)。
//! 该示例使用 axum 框架实现了基于 JWT 的身份验证机制,包含令牌生成和验证功能
//!
//! 运行方式:
//!
//! ```not_rust
//! JWT_SECRET=secret cargo run -p example-jwt
//! ```
// 导入必要的依赖库
// axum 用于构建 web 服务,提供路由、请求处理等功能
use axum::{
extract::FromRequestParts, // 用于从请求部分提取数据的 trait
http::{request::Parts, StatusCode}, // HTTP 相关类型,包括请求部分和状态码
response::{IntoResponse, Response}, // 响应处理相关 trait
routing::{get, post}, // 路由方法(GET、POST)
Json, RequestPartsExt, Router, // JSON 处理、请求部分扩展、路由构建器
};
// axum_extra 提供额外功能,这里用于处理 Authorization 头
use axum_extra::{
headers::{authorization::Bearer, Authorization}, // 处理 Bearer 类型的授权头
TypedHeader, // 类型化的 HTTP 头提取器
};
// jsonwebtoken 库用于 JWT 令牌的编码和解码
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
// serde 用于 JSON 序列化和反序列化
use serde::{Deserialize, Serialize};
// serde_json 用于 JSON 操作
use serde_json::json;
// 用于格式化输出
use std::fmt::Display;
// 用于线程安全的延迟初始化
use std::sync::LazyLock;
// 用于日志追踪
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
// 快速使用说明
//
//- 获取授权令牌:
//
//curl -s
// -w '\n'
// -H 'Content-Type: application/json'
// -d '{"client_id":"foo","client_secret":"bar"}'
// http://localhost:3000/authorize
//
//- 使用授权令牌访问受保护区域
//
//curl -s
// -w '\n'
// -H 'Content-Type: application/json'
// -H 'Authorization: Bearer <token>' # 替换为实际获取的令牌
// http://localhost:3000/protected
//
//- 尝试使用无效令牌访问受保护区域
//
//curl -s
// -w '\n'
// -H 'Content-Type: application/json'
// -H 'Authorization: Bearer blahblahblah'
// http://localhost:3000/protected
/// 全局 JWT 密钥存储
/// 使用 LazyLock 实现线程安全的延迟初始化,程序启动时从环境变量加载密钥
static KEYS: LazyLock<Keys> = LazyLock::new(|| {
// 从环境变量获取 JWT 密钥,若未设置则 panic
let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
Keys::new(secret.as_bytes())
});
/// 程序入口点
#[tokio::main] // 启用 tokio 运行时,支持异步操作
async fn main() {
// 初始化日志追踪系统
tracing_subscriber::registry()
.with(
// 从环境变量获取日志过滤配置,若未设置则使用默认配置
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| format!("{}=debug", env!("CARGO_CRATE_NAME")).into()),
)
.with(tracing_subscriber::fmt::layer()) // 添加日志格式化层
.init(); // 初始化追踪器
// 创建路由
let app = Router::new()
.route("/protected", get(protected)) // 注册 GET 方法的 /protected 路由,由 protected 函数处理
.route("/authorize", post(authorize)); // 注册 POST 方法的 /authorize 路由,由 authorize 函数处理
// 绑定到本地地址 127.0.0.1:3000
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap(); // 若绑定失败则 panic
tracing::debug!("listening on {}", listener.local_addr().unwrap()); // 输出监听地址
// 启动服务器,处理 incoming 连接
axum::serve(listener, app).await.unwrap();
}
/// 处理受保护路由的请求
/// 需要验证 JWT 令牌,成功后返回受保护内容
async fn protected(claims: Claims) -> Result<String, AuthError> {
// 向用户发送受保护的数据
Ok(format!(
"Welcome to the protected area :)\nYour data:\n{claims}",
))
}
/// 处理授权请求,验证客户端凭据并生成 JWT 令牌
async fn authorize(Json(payload): Json<AuthPayload>) -> Result<Json<AuthBody>, AuthError> {
// 检查客户端是否提供了凭据
if payload.client_id.is_empty() || payload.client_secret.is_empty() {
return Err(AuthError::MissingCredentials); // 缺少凭据时返回错误
}
// 验证客户端凭据(实际应用中应从数据库查询验证)
// 这里使用硬编码的 "foo" 和 "bar" 作为有效凭据
if payload.client_id != "foo" || payload.client_secret != "bar" {
return Err(AuthError::WrongCredentials); // 凭据错误时返回错误
}
// 创建 JWT 声明(Claims)
let claims = Claims {
sub: "b@b.com".to_owned(), // 主题(通常是用户标识,这里是邮箱)
company: "ACME".to_owned(), // 自定义字段:公司名称
exp: 2000000000, // 过期时间(Unix 时间戳),这里设置到 2033 年
};
// 生成 JWT 令牌
let token = encode(&Header::default(), &claims, &KEYS.encoding)
.map_err(|_| AuthError::TokenCreation)?; // 令牌生成失败时返回错误
// 返回包含令牌的 JSON 响应
Ok(Json(AuthBody::new(token)))
}
/// 为 Claims 实现 Display trait,用于格式化输出
impl Display for Claims {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Email: {}\nCompany: {}", self.sub, self.company)
}
}
/// AuthBody 结构体的辅助方法
impl AuthBody {
/// 创建新的 AuthBody 实例
fn new(access_token: String) -> Self {
Self {
access_token, // JWT 令牌
token_type: "Bearer".to_string(), // 令牌类型,固定为 "Bearer"
}
}
}
/// 为 Claims 实现 FromRequestParts trait,用于从请求中提取并验证 JWT 令牌
/// 实现后,Claims 可以作为处理函数的参数,自动从请求中提取
impl<S> FromRequestParts<S> for Claims
where
S: Send + Sync, // 状态类型需要满足 Send + Sync 约束
{
type Rejection = AuthError; // 提取失败时的错误类型
/// 从请求部分提取并验证 JWT 令牌,返回 Claims
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
// 从请求头中提取 Authorization: Bearer <token>
let TypedHeader(Authorization(bearer)) = parts
.extract::<TypedHeader<Authorization<Bearer>>>()
.await
.map_err(|_| AuthError::InvalidToken)?; // 提取失败时返回无效令牌错误
// 解码 JWT 令牌,验证其有效性
let token_data = decode::<Claims>(bearer.token(), &KEYS.decoding, &Validation::default())
.map_err(|_| AuthError::InvalidToken)?; // 解码失败时返回无效令牌错误
Ok(token_data.claims) // 返回解码后的声明
}
}
/// 为 AuthError 实现 IntoResponse trait,将错误转换为 HTTP 响应
impl IntoResponse for AuthError {
fn into_response(self) -> Response {
// 根据错误类型确定 HTTP 状态码和错误消息
let (status, error_message) = match self {
AuthError::WrongCredentials => (StatusCode::UNAUTHORIZED, "Wrong credentials"),
AuthError::MissingCredentials => (StatusCode::BAD_REQUEST, "Missing credentials"),
AuthError::TokenCreation => (StatusCode::INTERNAL_SERVER_ERROR, "Token creation error"),
AuthError::InvalidToken => (StatusCode::BAD_REQUEST, "Invalid token"),
};
// 构建 JSON 响应体
let body = Json(json!({
"error": error_message,
}));
// 组合状态码和响应体,转换为 Response
(status, body).into_response()
}
}
/// 存储 JWT 编码和解码所需的密钥
struct Keys {
encoding: EncodingKey, // 用于生成 JWT 的编码密钥
decoding: DecodingKey, // 用于验证 JWT 的解码密钥
}
impl Keys {
/// 创建新的 Keys 实例
/// 从字节数组初始化编码和解码密钥(JWT 通常使用相同的密钥进行签名和验证)
fn new(secret: &[u8]) -> Self {
Self {
encoding: EncodingKey::from_secret(secret),
decoding: DecodingKey::from_secret(secret),
}
}
}
/// JWT 中的声明(Claims)结构体
/// 包含需要在令牌中传递的信息,会被序列化到 JWT 中
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String, // 标准字段:主题(subject)
company: String, // 自定义字段:公司名称
exp: usize, // 标准字段:过期时间(expiration time),Unix 时间戳
}
/// 认证响应结构体
/// 包含生成的 JWT 令牌和令牌类型
#[derive(Debug, Serialize)]
struct AuthBody {
access_token: String, // JWT 访问令牌
token_type: String, // 令牌类型(通常为 "Bearer")
}
/// 认证请求负载结构体
/// 用于接收客户端发送的认证凭据
#[derive(Debug, Deserialize)]
struct AuthPayload {
client_id: String, // 客户端 ID
client_secret: String, // 客户端密钥
}
/// 认证相关错误枚举
/// 定义了各种可能的认证错误类型
#[derive(Debug)]
enum AuthError {
WrongCredentials, // 凭据错误
MissingCredentials, // 缺少凭据
TokenCreation, // 令牌创建失败
InvalidToken, // 无效的令牌
}
更多推荐
所有评论(0)