跳到主要内容

2 篇博文 含有标签「JWT」

查看所有标签

本文将介绍如何使用 Axum 框架实现 JWT 授权,包括从生成密钥对、创建和验证 Token 到在 Axum 中实现授权中间件,全面讲解构建安全 API 的流程。

Axum 是一个基于 Hyper 的 Rust Web 框架,它提供了高效、灵活的方式来构建现代 Web 应用。在大多数实际应用中,我们需要对 API 端点进行授权,以确保只有授权用户才能访问受保护的资源。**JWT(JSON Web Token)**是一种流行的授权机制,非常适合用于这种场景,能够实现无状态、跨平台的身份验证。

本文大体分为三个部分:

  1. 生成一个新的 Ed25519 公私钥对
  2. 生成和验证 JWT Token
  3. 在 Axum 中集成授权中间件

1. 生成 Ed25519 的公私钥​

在我们的授权系统中,我们使用 Ed25519 算法来生成公私钥对。这是一种现代的、安全的数字签名算法,提供了高安全性和高性能,非常适合用在 JWT 授权中。

生成公私钥的代码示例​

use anyhow::Result;
use jwt_simple::prelude::*;
use std::fs::File;

fn main() -> Result<()> {
generate_and_save_keys()?;
Ok(())
}

fn generate_and_save_keys() -> Result<()> {
let key_pair = Ed25519KeyPair::generate();

// 保存私钥
let private_key_pem = key_pair.to_pem();
let mut private_key_file = File::create("private_key.pem")?;
private_key_file.write_all(private_key_pem.as_bytes())?;

// 保存公钥
let public_key_pem = key_pair.public_key().to_pem();
let mut public_key_file = File::create("public_key.pem")?;
public_key_file.write_all(public_key_pem.as_bytes())?;

Ok(())
}

代码说明:

  • 使用 Ed25519KeyPair::generate() 生成密钥对,并分别保存公钥和私钥。
  • 私钥用于生成 Token,必须严格保密,而公钥用于验证 Token,可以公开发布。
  • PEM 格式是一种常用的密钥存储格式,易于管理。
提示

将公钥和私钥分开存储可以保证系统安全性。私钥通常只在生成 Token 时使用,而公钥则用于对 Token 的验证,可以公开提供给需要验证身份的服务。

2. 生成 JWT Token 和验证 Token​

在这一步,我们会学习如何生成和验证 JWT Token,帮助我们在 Web 应用中实现授权控制。

生成 Token 的实现​

fn sign(user: impl Into<User>) -> Result<String> {
let private_key_pem = read_to_string("private_key.pem")?;
let key_pair = Ed25519KeyPair::from_pem(&private_key_pem)?;

let user = user.into();
let claims = Claims::with_custom_claims(user, Duration::from_secs(JWT_DURATION));
let claims = claims.with_issuer(JWT_ISS).with_audience(JWT_AUD);

let token = key_pair.sign(claims)?;
Ok(token)
}

代码说明:

  1. 从文件中读取私钥,并创建 Ed25519KeyPair。
  2. 创建一个包含用户信息的 claims 对象,并设置 Token 的有效期、发行者和受众。
  3. 使用私钥对 claims 进行签名,生成 Token。

验证 Token 的实现​

fn verify(token: &str) -> Result<User, Box<dyn std::error::Error>> {
let public_key_pem = read_to_string("public_key.pem")?;
let public_key = Ed25519PublicKey::from_pem(&public_key_pem)?;

let options = VerificationOptions {
allowed_issuers: Some(HashSet::from_strings(&[JWT_ISS])),
allowed_audiences: Some(HashSet::from_strings(&[JWT_AUD])),
..Default::default()
};

let claims = public_key.verify_token::<User>(token, Some(options))?;
Ok(claims.custom)
}

验证步骤:

  • 使用公钥验证 Token 的签名,并检查是否符合预期的发行者和受众。
  • 通过验证后,返回自定义的 User 数据。

要点:

  • 生成 Token 时使用私钥,验证 Token 时使用公钥。
  • 设置有效期、发行者和受众,以提高安全性。

3. 在 Axum 中集成 JWT 授权中间件​

为了确保只有经过身份验证的用户才能访问受保护的 API,我们需要将 JWT 的生成和验证集成到 Axum 框架中。

定义 AuthUser 结构体并实现 FromRequestParts Trait​

struct AuthUser(User);

#[async_trait]
impl<S> FromRequestParts<S> for AuthUser
where
S: Send + Sync,
{
type Rejection = StatusCode;

async fn from_request_parts<'life0, 'life1>(
parts: &'life0 mut Parts,
_state: &'life1 S,
) -> Result<Self, Self::Rejection>
where
'life0: 'async_trait,
'life1: 'async_trait,
Self: 'async_trait,
{
let token = parts
.headers
.get("Authorization")
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.ok_or(StatusCode::UNAUTHORIZED)?;
let user = verify(token).map_err(|_| StatusCode::UNAUTHORIZED)?;
Ok(AuthUser(user))
}
}

该实现可以让我们方便地从请求中提取出认证信息。

创建授权中间件​

async fn auth_middleware(
AuthUser(user): AuthUser,
req: Request<Body>,
next: Next,
) -> impl IntoResponse {
info!("Authenticated user: {}", user.username);
next.run(req).await
}

这个中间件会记录用户信息,然后继续处理请求,确保只有认证过的用户可以访问受保护的路由。

在 Axum 中使用中间件​

let app = Router::new()
.route("/login", post(get_token))
.route("/protected", get(protected_route).layer(from_fn(auth_middleware)));

我们定义了两个路由:

  • /login:用于登录并获取 Token,不需要认证。
  • /protected:受保护的路由,需要通过授权中间件的认证。

登录并获取 Token 的实现​

async fn get_token(
Json(payload): Json<TokenRequest>
) -> Result<Json<TokenResponse>, StatusCode> {
let user = User {
username: payload.username,
created_at: Utc::now(),
scope: vec!["read".to_string(), "write".to_string()],
};
let token = sign(user).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(TokenResponse { token }))
}

该函数用于处理登录请求,生成并返回 JWT Token。

受保护路由的实现​

async fn protected_route(AuthUser(user): AuthUser) -> impl IntoResponse {
format!("Hello, {}! Your scopes are: {:?}", user.username, user.scope)
}

该路由只有在用户通过认证后才能访问,返回用户的相关信息。

要点:

  • 使用 FromRequestParts Trait 从请求中提取认证信息,便于集成 JWT 认证。
  • 中间件统一处理认证逻辑,提高了代码的模块化和可维护性。

测试 JWT 授权​

使用 REST 客户端(例如 VSCode REST Client 插件)可以测试整个授权流程:

  1. 获取 Token:首先发送 POST 请求到 /login。
  2. 访问受保护的路由:使用获得的 Token,设置 Authorization 请求头访问 /protected。
### Get Token
POST http://localhost:3000/login
Content-Type: application/json

{
"username": "admin"
}

@token = {{signin.response.body.token}}

### Auth user
GET http://localhost:3000/protected
Authorization: Bearer {{token}}
备注

在这里,我们使用了 REST Client 插件来进行测试,方便地将获取的 Token 用于后续请求,避免手动复制粘贴。

完整代码示例​

Cargo.toml 依赖库​

[package]
name = "axum-jwt-auth"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.86"
axum = "0.7.5"
chrono = { version = "0.4.38", features = ["serde"] }
jwt-simple = "0.12.9"
serde = { version = "1.0.204", features = ["derive"] }
tokio = { version = "1.39.1", features = ["net", "rt", "rt-multi-thread"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }

主要 Rust 代码示例​

use anyhow::Result;
use chrono::{DateTime, Utc};
use jwt_simple::prelude::*;
use serde::{Deserialize, Serialize};
use tokio::net::TcpListener;

use tracing::{info, level_filters::LevelFilter};
use tracing_subscriber::{
fmt::Layer, layer::SubscriberExt,
util::SubscriberInitExt,
Layer as _
};

use std::{
collections::HashSet,
fs::{read_to_string, File},
io::Write,
net::SocketAddr,
};

use axum::{
async_trait, body::Body,
extract::FromRequestParts,
http::{request::Parts, Request,StatusCode},
middleware::{from_fn, Next},
response::IntoResponse,
routing::{get, post},
Json, Router
};

// 省略部分代码,可参考上文详细内容

结论​

通过这篇文章,我们了解了如何使用 Axum 框架在 Rust 中实现基于 JWT 的授权系统。具体内容包括:

  • 使用 Ed25519 算法生成密钥对,确保私钥的安全性。
  • 生成和验证 JWT Token,以实现无状态的授权。
  • 将 Token 签发和验证集成到 Axum 中,确保对 API 端点进行保护。

这种方法为我们提供了一个灵活、安全的 API 保护机制。在实际应用中,你还可以引入更多的安全措施,如 Token 刷新、权限管理、密钥轮换等,以进一步提升安全性。

参考链接​

鱼雪

JWT Token Encode

JWT(JSON Web Token)是一种用于标识和验证经过身份验证用户的安全令牌,通常用于 身份验证 和 授权。它们由身份验证服务器签发,并由客户端和服务器使用来保护 API,确保信息的安全传输和身份的准确验证。

本文将为您全面讲解 JWT 的工作原理、结构、声明规范、优缺点 以及在 开发过程中常见的问题,帮助您深入理解并有效使用 JSON Web Token。

什么是JWT?​

JSON Web 令牌(JWT)是一种开放的行业标准,用于在两个实体之间共享信息,通常是客户端(如您的应用前端)和服务器(如您的应用后端)。JWT 中包含的 JSON 数据被加密签名,确保信息不能被篡改,并可通过公私钥验证其真实性。

例如,当您使用 Google 登录某个应用时,Google 会生成并返回一个 JWT,如下:

{
"iss": "https://accounts.google.com",
"aud": "1234987819200.apps.googleusercontent.com",
"sub": "10769150350006150715113082367",
"email": "jsmith@example.com",
"email_verified": true,
"iat": 1353601026,
"exp": 1353604926
}

通过这个令牌,您的应用就可以准确了解用户的身份信息并验证其真实性。

为什么需要使用JWT?​

您可能会想,为什么不直接发送一个 JSON 对象,而需要使用令牌的形式呢?JWT 的安全性 是其主要优势。

如果身份验证服务器仅发送纯 JSON 数据,那么恶意用户可以对其内容进行修改,而客户端无法判断数据的真实性。因此,JWT 通过加密和签名机制,确保传递的数据不能被篡改,并且可以在接收端通过验证机制确保数据的可信度。

简单来说,JWT 是一个安全的字符串,包含共享信息,并且能够验证该信息的完整性和来源。

JWT的结构​

一个 JWT 由三部分组成:

  1. Header(头部):包含签名算法类型和令牌类型,通常为 JWT。
  2. Payload(负载):包含声明(Claim)或用户相关的 JSON 数据。
  3. Signature(签名):通过加密算法生成,用于验证令牌的完整性和真实性。

JWT 使用 <header>.<payload>.<signature> 的格式进行编码和传递,确保数据在传输过程中保持完整且安全。

JWT 声明约定​

JWT 的负载部分包含声明,声明可以是标准化的或自定义的。常见的声明包括:

  • iss(Issuer):签发者,例如 https://accounts.google.com。
  • aud(Audience):令牌的受众,通常为应用的客户端 ID。
  • sub(Subject):用户的唯一标识符。
  • iat(Issued At):令牌的签发时间。
  • exp(Expiration Time):令牌的过期时间,确保令牌在一定时间后失效。

JWT 的工作原理(示例)​

以下是一个简单的示例,演示如何生成和验证 JWT:

1. 创建 JSON 负载​

首先,我们创建一个包含用户信息的 JSON 负载:

{
"userId": "abcd123",
"expiry": 1646635611301
}

2. 选择签名密钥和签名算法​

签名密钥可以是一个随机生成的字符串,算法可以选择 HMAC + SHA256,即 HS256。

3. 生成 Header​

Header 通常包含签名算法类型,例如:

{
"typ": "JWT",
"alg": "HS256"
}

4. 创建签名​

我们将 Header 和 Payload 分别进行 base64 编码,然后使用密钥和算法生成签名。最终的 JWT 格式为:

<header>.<payload>.<signature>

5. 验证 JWT​

在服务器端,JWT 需要经过以下验证步骤:

  1. 验证签名是否与密钥匹配。
  2. 检查 exp(过期时间)是否仍然有效。
  3. 确保所有声明(例如 iss、aud)均符合要求。

JWT 的优缺点​

优点​

  • 安全性:JWT 使用签名机制,确保数据不能被篡改。
  • 无状态和高效:JWT 是自包含的,服务器端无需存储用户会话状态,极大提高了系统的扩展性。
  • 灵活性:JWT 可以轻松在多个应用程序之间共享用户身份信息,适合于微服务架构。

缺点​

  • 不可撤销:JWT 一旦签发便无法撤销,直到过期。虽然可以使用黑名单机制,但管理复杂。
  • 密钥依赖:JWT 的安全性依赖于签名密钥的安全,一旦泄露,攻击者可以伪造合法令牌。

开发中的常见问题​

1. JWT 被拒绝​

此错误通常表示 JWT 验证失败,原因可能是:

  • JWT 已过期:需要检查 exp 字段。
  • 签名不匹配:签名密钥不一致,或数据被篡改。
  • 无效的声明:例如,令牌的 aud(受众)不匹配预期应用。

2. JWT 不支持所需的范围​

JWT 中可以包含用户的权限范围,若应用程序请求的权限超出了 JWT 提供的权限范围,便会导致错误。这时需要在令牌中包含更多的范围信息,或者请求用户进行授权升级。

3. JWT 解码失败​

此错误可能是由于 JWT 格式错误,例如令牌中的某部分未正确 base64 编码。需要确保生成和传输过程中 JWT 的完整性和正确性。

进一步的阅读​

鱼雪