大家好!今天我们来聊聊用 Rust 做 Web 开发的那些事儿。说到 Rust Web 框架,Axum 绝对是当前最热门的选择之一。它不仅性能出色,而且设计优雅,特别适合构建高性能的 API 服务。本文将带你从零开始,用 Axum 实现一个完整的 Web 应用,涵盖路由、数据库连接池和 JWT 认证三大核心功能。

1. 准备工作与环境搭建

首先,我们需要创建一个新的 Rust 项目。打开终端,运行:

cargo new axum_web_app --bin
cd axum_web_app

然后编辑 Cargo.toml 文件,添加我们需要的依赖项:

[dependencies]
axum = "0.7"          # Web 框架
tokio = { version = "1.0", features = ["full"] }  # 异步运行时
serde = { version = "1.0", features = ["derive"] }  # 序列化/反序列化
serde_json = "1.0"    # JSON 处理
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio"] }  # 数据库工具
dotenv = "0.15"       # 环境变量加载
jsonwebtoken = "9.0"  # JWT 处理
chrono = { version = "0.4", features = ["serde"] }  # 日期时间处理
bcrypt = "0.15"       # 密码哈希
tower-http = { version = "0.5", features = ["cors"] }  # 中间件

这里我们选择了 PostgreSQL 作为数据库,如果你喜欢 MySQL 或 SQLite,可以相应调整 sqlx 的 features。

2. Axum 路由系统详解

Axum 的路由系统设计得非常直观,让我们来看一个完整的示例:

use axum::{
    routing::{get, post},
    Router,
    response::Json,
    extract::{Path, Query, State},
    http::StatusCode,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;

// 定义应用状态
#[derive(Clone)]
struct AppState {
    db_pool: sqlx::PgPool,
}

// 用户数据结构
#[derive(Debug, Serialize, Deserialize)]
struct User {
    id: i32,
    username: String,
    email: String,
}

// 创建路由
async fn create_router(db_pool: sqlx::PgPool) -> Router {
    let shared_state = Arc::new(AppState { db_pool });
    
    Router::new()
        // 基本路由示例
        .route("/", get(root))
        // 带路径参数的路由
        .route("/users/:id", get(get_user))
        // 带查询参数的路由
        .route("/search", get(search_users))
        // 需要状态的路由
        .route("/protected", get(protected_route))
        // 嵌套路由
        .nest("/api", api_routes())
        // 添加共享状态
        .with_state(shared_state)
}

// 嵌套路由示例
fn api_routes() -> Router {
    Router::new()
        .route("/users", get(get_all_users))
        .route("/users", post(create_user))
}

// 根路由处理函数
async fn root() -> &'static str {
    "欢迎来到 Axum Web 应用!"
}

// 带路径参数的处理函数
async fn get_user(Path(user_id): Path<i32>) -> Result<Json<User>, StatusCode> {
    // 实际应用中这里会查询数据库
    let user = User {
        id: user_id,
        username: "test_user".to_string(),
        email: "test@example.com".to_string(),
    };
    Ok(Json(user))
}

// 带查询参数的处理函数
async fn search_users(Query(params): Query<SearchParams>) -> Json<Vec<User>> {
    // 实际应用中会根据查询参数过滤用户
    Json(vec![
        User {
            id: 1,
            username: "user1".to_string(),
            email: "user1@example.com".to_string(),
        },
        User {
            id: 2,
            username: "user2".to_string(),
            email: "user2@example.com".to_string(),
        },
    ])
}

// 查询参数结构
#[derive(Debug, Deserialize)]
struct SearchParams {
    q: String,
    limit: Option<u32>,
}

// 需要状态的处理函数
async fn protected_route(State(state): State<Arc<AppState>>) -> Result<String, StatusCode> {
    // 这里可以访问共享的数据库连接池
    let _pool = &state.db_pool;
    Ok("这是一个受保护的路由".to_string())
}

// 获取所有用户
async fn get_all_users() -> Json<Vec<User>> {
    // 实际应用中会从数据库获取
    Json(vec![])
}

// 创建用户
async fn create_user() -> Json<User> {
    // 实际应用中会处理请求体并保存到数据库
    Json(User {
        id: 1,
        username: "new_user".to_string(),
        email: "new@example.com".to_string(),
    })
}

Axum 的路由系统有几个显著特点:

  1. 类型安全:路径参数、查询参数都会被正确解析并验证类型
  2. 组合性强:可以轻松嵌套路由,构建复杂的 API 结构
  3. 状态共享:通过 with_state 可以安全地在路由间共享数据
  4. 中间件友好:可以方便地添加各种中间件

3. 数据库连接池实现

数据库连接池是 Web 应用的关键组件,它能显著提高数据库访问性能。下面我们看看如何使用 sqlx 实现 PostgreSQL 连接池:

use sqlx::postgres::PgPoolOptions;
use dotenv::dotenv;
use std::env;

// 初始化数据库连接池
async fn init_db_pool() -> Result<sqlx::PgPool, Box<dyn std::error::Error>> {
    // 加载 .env 文件
    dotenv().ok();
    
    // 从环境变量获取数据库URL
    let database_url = env::var("DATABASE_URL")
        .expect("DATABASE_URL 必须在 .env 文件中设置");
    
    // 创建连接池
    let pool = PgPoolOptions::new()
        .max_connections(10)  // 最大连接数
        .acquire_timeout(std::time::Duration::from_secs(5))  // 获取连接超时时间
        .connect(&database_url)
        .await?;
    
    // 运行迁移
    sqlx::migrate!("./migrations")
        .run(&pool)
        .await?;
    
    Ok(pool)
}

// 用户模型操作
mod user_model {
    use super::*;
    
    // 获取用户
    pub async fn get_user(
        pool: &sqlx::PgPool,
        user_id: i32
    ) -> Result<User, sqlx::Error> {
        sqlx::query_as!(
            User,
            "SELECT id, username, email FROM users WHERE id = $1",
            user_id
        )
        .fetch_one(pool)
        .await
    }
    
    // 创建用户
    pub async fn create_user(
        pool: &sqlx::PgPool,
        username: &str,
        email: &str,
        password: &str
    ) -> Result<User, sqlx::Error> {
        let hashed_password = bcrypt::hash(password, bcrypt::DEFAULT_COST)?;
        
        let user = sqlx::query_as!(
            User,
            r#"
            INSERT INTO users (username, email, password_hash)
            VALUES ($1, $2, $3)
            RETURNING id, username, email
            "#,
            username,
            email,
            hashed_password
        )
        .fetch_one(pool)
        .await?;
        
        Ok(user)
    }
    
    // 验证用户
    pub async fn verify_user(
        pool: &sqlx::PgPool,
        username: &str,
        password: &str
    ) -> Result<User, sqlx::Error> {
        let user = sqlx::query_as!(
            UserWithPassword,
            "SELECT id, username, email, password_hash FROM users WHERE username = $1",
            username
        )
        .fetch_one(pool)
        .await?;
        
        if bcrypt::verify(password, &user.password_hash)? {
            Ok(User {
                id: user.id,
                username: user.username,
                email: user.email,
            })
        } else {
            Err(sqlx::Error::RowNotFound)
        }
    }
    
    #[derive(Debug)]
    struct UserWithPassword {
        id: i32,
        username: String,
        email: String,
        password_hash: String,
    }
}

数据库连接池的最佳实践:

  1. 合理设置连接数:通常设置为 CPU 核心数的 2-3 倍
  2. 使用环境变量:不要将数据库凭据硬编码在代码中
  3. 自动迁移:使用 sqlx::migrate! 宏管理数据库迁移
  4. 错误处理:妥善处理数据库操作可能出现的各种错误

4. JWT 认证完整实现

JWT (JSON Web Token) 是现代 Web 应用中常用的认证机制。下面我们实现一个完整的 JWT 认证流程:

use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
use chrono::{Utc, Duration};
use axum::{extract::FromRequestParts, http::request::Parts};

// JWT 配置
struct JwtConfig {
    secret: String,
    expiration_hours: i64,
}

// 声明
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: i32,  // 用户ID
    exp: usize,  // 过期时间
    username: String,
}

// 认证错误
#[derive(Debug)]
enum AuthError {
    InvalidToken,
    MissingToken,
}

// 实现 JWT 生成
fn generate_jwt(user_id: i32, username: &str, config: &JwtConfig) -> String {
    let expiration = Utc::now()
        .checked_add_signed(Duration::hours(config.expiration_hours))
        .expect("有效时间计算失败")
        .timestamp() as usize;
    
    let claims = Claims {
        sub: user_id,
        exp: expiration,
        username: username.to_owned(),
    };
    
    encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(config.secret.as_ref()),
    )
    .unwrap()
}

// 实现 JWT 验证
fn validate_jwt(token: &str, secret: &str) -> Result<Claims, AuthError> {
    decode::<Claims>(
        token,
        &DecodingKey::from_secret(secret.as_ref()),
        &Validation::default(),
    )
    .map(|data| data.claims)
    .map_err(|_| AuthError::InvalidToken)
}

// 实现 Axum 的认证提取器
#[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> {
        let auth_header = parts.headers
            .get("Authorization")
            .and_then(|header| header.to_str().ok())
            .ok_or(AuthError::MissingToken)?;
        
        if !auth_header.starts_with("Bearer ") {
            return Err(AuthError::InvalidToken);
        }
        
        let token = auth_header.trim_start_matches("Bearer ").trim();
        let secret = std::env::var("JWT_SECRET").unwrap_or_default();
        
        validate_jwt(token, &secret)
    }
}

// 受保护路由示例
async fn protected_route_handler(claims: Claims) -> String {
    format!(
        "欢迎 {}, 你的用户ID是 {}",
        claims.username, claims.sub
    )
}

// 登录路由
async fn login_handler(
    State(state): State<Arc<AppState>>,
    Json(login_data): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, StatusCode> {
    let user = user_model::verify_user(&state.db_pool, &login_data.username, &login_data.password)
        .await
        .map_err(|_| StatusCode::UNAUTHORIZED)?;
    
    let jwt_config = JwtConfig {
        secret: std::env::var("JWT_SECRET").unwrap_or_else(|_| "secret".to_string()),
        expiration_hours: 24,
    };
    
    let token = generate_jwt(user.id, &user.username, &jwt_config);
    
    Ok(Json(LoginResponse {
        token,
        user,
    }))
}

#[derive(Debug, Deserialize)]
struct LoginRequest {
    username: String,
    password: String,
}

#[derive(Debug, Serialize)]
struct LoginResponse {
    token: String,
    user: User,
}

JWT 实现的关键点:

  1. 安全存储密钥:JWT 密钥应该通过环境变量配置
  2. 合理设置过期时间:通常几小时到几天不等
  3. 使用强加密算法:默认 HS256 已经足够安全
  4. 妥善处理错误:区分各种认证失败情况

5. 完整应用示例

现在我们把所有部分组合起来,创建一个完整的应用:

use axum::{
    Router,
    routing::{get, post},
    Extension,
    response::IntoResponse,
    Json,
};
use std::{sync::Arc, net::SocketAddr};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 初始化数据库连接池
    let db_pool = init_db_pool().await?;
    
    // 创建应用状态
    let shared_state = Arc::new(AppState {
        db_pool,
    });
    
    // 创建路由
    let app = Router::new()
        .route("/", get(root))
        .route("/login", post(login_handler))
        .route("/protected", get(protected_route_handler))
        .nest("/api", api_routes())
        .with_state(shared_state);
    
    // 启动服务器
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("服务器运行在 {}", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await?;
    
    Ok(())
}

6. 应用场景与技术分析

应用场景

这种技术栈特别适合以下场景:

  1. 高性能 API 服务:需要处理大量并发请求
  2. 微服务架构:作为系统中的某个服务
  3. 需要强类型安全的项目:Rust 的编译器能捕获许多运行时错误
  4. 长期维护的项目:Rust 的稳定性保证长期兼容性

技术优缺点

优点:

  1. 卓越的性能:Rust 和 Axum 的组合提供了接近 C 的性能
  2. 内存安全:没有数据竞争和空指针异常
  3. 强大的类型系统:编译时捕获大量错误
  4. 丰富的生态系统:虽然不如 Node.js 或 Python 庞大,但关键组件都很成熟

缺点:

  1. 学习曲线陡峭:Rust 的所有权系统需要时间适应
  2. 编译时间长:特别是首次编译依赖项时
  3. 异步编程复杂:需要理解 Rust 的异步模型
  4. 生态系统成熟度:某些领域可能缺少成熟的库

注意事项

  1. 数据库连接泄露:确保每次获取连接后都正确释放
  2. JWT 安全:使用足够复杂的密钥并定期更换
  3. 错误处理:为所有可能的错误情况提供有意义的响应
  4. 性能监控:在生产环境中监控关键指标
  5. 日志记录:实现全面的日志记录以方便调试

7. 总结

通过本文,我们实现了一个完整的 Rust Web 应用,涵盖了 Axum 路由、数据库连接池和 JWT 认证三大核心功能。Rust 的强类型系统和 Axum 的优雅设计使得构建安全、高效的 Web 服务变得既有趣又可靠。

虽然 Rust Web 开发的生态系统还在成长中,但对于追求性能和安全性的项目来说,它已经是一个非常有力的选择。特别是对于需要长期维护、对性能要求高的项目,Rust 的优势会更加明显。

希望这篇文章能帮助你入门 Rust Web 开发。如果你有任何问题或建议,欢迎在评论区讨论!