Skip to main content

2 posts tagged with "JWT"

View All Tags

本文将介绍如何使用 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 格式是一种常用的密钥存储格式,易于管理。
tip

将公钥和私钥分开存储可以保证系统安全性。私钥通常只在生成 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}}
note

在这里,我们使用了 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

JWTJSON 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. 创建签名

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

<header>.<payload>.<signature>

5. 验证 JWT

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

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

JWT 的优缺点

优点

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

缺点

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

开发中的常见问题

1. JWT 被拒绝

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

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

2. JWT 不支持所需的范围

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

3. JWT 解码失败

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

进一步的阅读

鱼雪