大家好!今天我们来聊聊用 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 的路由系统有几个显著特点:
- 类型安全:路径参数、查询参数都会被正确解析并验证类型
- 组合性强:可以轻松嵌套路由,构建复杂的 API 结构
- 状态共享:通过
with_state可以安全地在路由间共享数据 - 中间件友好:可以方便地添加各种中间件
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,
}
}
数据库连接池的最佳实践:
- 合理设置连接数:通常设置为 CPU 核心数的 2-3 倍
- 使用环境变量:不要将数据库凭据硬编码在代码中
- 自动迁移:使用
sqlx::migrate!宏管理数据库迁移 - 错误处理:妥善处理数据库操作可能出现的各种错误
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 实现的关键点:
- 安全存储密钥:JWT 密钥应该通过环境变量配置
- 合理设置过期时间:通常几小时到几天不等
- 使用强加密算法:默认 HS256 已经足够安全
- 妥善处理错误:区分各种认证失败情况
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. 应用场景与技术分析
应用场景
这种技术栈特别适合以下场景:
- 高性能 API 服务:需要处理大量并发请求
- 微服务架构:作为系统中的某个服务
- 需要强类型安全的项目:Rust 的编译器能捕获许多运行时错误
- 长期维护的项目:Rust 的稳定性保证长期兼容性
技术优缺点
优点:
- 卓越的性能:Rust 和 Axum 的组合提供了接近 C 的性能
- 内存安全:没有数据竞争和空指针异常
- 强大的类型系统:编译时捕获大量错误
- 丰富的生态系统:虽然不如 Node.js 或 Python 庞大,但关键组件都很成熟
缺点:
- 学习曲线陡峭:Rust 的所有权系统需要时间适应
- 编译时间长:特别是首次编译依赖项时
- 异步编程复杂:需要理解 Rust 的异步模型
- 生态系统成熟度:某些领域可能缺少成熟的库
注意事项
- 数据库连接泄露:确保每次获取连接后都正确释放
- JWT 安全:使用足够复杂的密钥并定期更换
- 错误处理:为所有可能的错误情况提供有意义的响应
- 性能监控:在生产环境中监控关键指标
- 日志记录:实现全面的日志记录以方便调试
7. 总结
通过本文,我们实现了一个完整的 Rust Web 应用,涵盖了 Axum 路由、数据库连接池和 JWT 认证三大核心功能。Rust 的强类型系统和 Axum 的优雅设计使得构建安全、高效的 Web 服务变得既有趣又可靠。
虽然 Rust Web 开发的生态系统还在成长中,但对于追求性能和安全性的项目来说,它已经是一个非常有力的选择。特别是对于需要长期维护、对性能要求高的项目,Rust 的优势会更加明显。
希望这篇文章能帮助你入门 Rust Web 开发。如果你有任何问题或建议,欢迎在评论区讨论!
评论