本文将介绍如何使用 Axum 框架实现 JWT 授权,包括从生成密钥对、创建和验证 Token 到在 Axum 中实现授权中间件,全面讲解构建安全 API 的流程。
Axum 是一个基于 Hyper 的 Rust Web 框架,它提供了高效、灵活的方式来构建现代 Web 应用。在大多数实际应用中,我们需要对 API 端点进行授权,以确保只有授权用户才能访问受保护的资源。**JWT(JSON Web Token)**是一种流行的授权机制,非常适合用于这种场景,能够实现无状态、跨平台的身份验证。
本文大体分为三个部分:
- 生成一个新的 Ed25519 公私钥对
- 生成和验证 JWT Token
- 在 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)
}
代码说明:
- 从文件中读取私钥,并创建
Ed25519KeyPair
。 - 创建一个包含用户信息的
claims
对象,并设置 Token 的有效期、发行者和受众。 - 使用私钥对
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 插件)可以测试整个授权流程:
- 获取 Token:首先发送 POST 请求到
/login
。 - 访问受保护的路由:使用获得的 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}}