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,  // 无效的令牌
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐