跳到主要内容
Rust Web 开发实战:从 Actix-web 框架到 RESTful API 完整构建 | 极客日志
Rust
Rust Web 开发实战:从 Actix-web 框架到 RESTful API 完整构建 Rust Web 开发实战教程,深入对比 Actix-web、Rocket、Axum 三大框架,重点讲解 Actix-web 的项目初始化、路由定义、参数解析及中间件配置。内容涵盖 SQLx 类型安全数据库操作、JWT 认证中间件实现、任务管理系统 CRUD 实战,以及 Docker 容器化部署与 Prometheus+Grafana 监控方案。适合希望构建高性能 Rust Web 应用的开发者参考。
片刻 发布于 2026/2/24 更新于 2026/5/31 25 浏览Rust Web 开发实战:从 Actix-web 框架到 RESTful API 完整构建
一、学习目标与重点
1.1 学习目标
框架选型对比 :深入了解 Actix-web、Rocket、Axum 三大主流 Rust Web 框架的架构差异、性能表现与适用场景。
Actix-web 核心用法 :熟练掌握项目初始化、路由定义(GET/POST/PUT/DELETE)、请求参数解析(Query/Path/Json/Form)、响应构建(Json/Plain/File)。
中间件开发与使用 :学习内置中间件(日志、CORS)的配置,以及自定义中间件的实现方法(如 JWT 认证、限流)。
数据库操作与集成 :使用 SQLx 异步连接 PostgreSQL/MySQL,实现类型安全的 SQL 查询、事务管理。
完整 API 构建实战 :结合真实场景编写任务管理系统 API,包含用户认证(JWT)、任务 CRUD、分页查询、文件上传等功能。
部署与监控 :掌握 Docker 容器化部署(Dockerfile+docker-compose),以及 Prometheus+Grafana 监控的配置。
1.2 学习重点
💡 三大核心难点 :
Actix-web 的 Actor 模型与请求上下文 :理解 Actix-web 基于 Actor 的请求处理流程,掌握 State、Data、Extensions 的区别与使用。
类型安全的 SQL 查询 :使用 SQLx 的查询宏(query/query_as),避免 SQL 注入,处理查询结果的错误。
JWT 认证与权限控制 :实现 JWT 的生成、验证、刷新,以及基于角色的权限控制。
⚠️ 三大高频错误点 :
CORS 跨域配置错误 :未正确配置 CORS 中间件,导致前端无法发送请求。
数据库连接池配置不当 :连接池大小设置不合理,导致高并发下的性能问题。
JWT 令牌过期未处理 :未实现令牌刷新机制,导致用户频繁重新登录。
二、Rust Web 框架选型
Rust 生态中有很多优秀的 Web 框架,其中最主流的是Actix-web 、Rocket 、Axum 。
2.1 框架对比
框架 架构 性能 文档 易用性 生态 适用场景 Actix-web Actor 模型(Actix actor system)+ 异步运行时(Tokio) 🌟🌟🌟🌟🌟(最高) 🌟🌟🌟🌟 🌟🌟🌟 🌟🌟🌟🌟 高并发、低延迟的 Web 应用(API 网关、实时通信服务器)
宏驱动(属性路由)+ 同步/异步运行时(Rocket runtime/Tokio)
Axum Tokio 的 Service 模型 + 异步运行时(Tokio)+ 宏驱动(属性/函数路由) 🌟🌟🌟🌟 🌟🌟🌟 🌟🌟🌟 🌟🌟🌟 中等并发的 Web 应用、API
2.2 框架选择建议
如果你需要最高性能 的 Web 应用,请选择Actix-web 。
如果你需要最快开发速度 ,请选择Rocket 。
如果你需要最简单的架构 和最稳定的生态 ,请选择Axum 。
三、Actix-web 基础
3.1 项目初始化与依赖配置 ① 安装 Cargo(Rust 的包管理工具)。
② 创建新的 Rust 项目:
cargo new task-management-api
cd task-management-api
③ 在 Cargo.toml 中添加 Actix-web 的依赖:
[dependencies]
actix-web = "4"
actix-rt = "2"
serde = { version = "1.0" , features = ["derive" ] }
serde_json = "1.0"
sqlx = { version = "0.7" , features = ["postgres" , "runtime-tokio-native-tls" ] }
tokio = { version = "1" , features = ["full" ] }
juniper = "0.15"
3.2 路由定义 Actix-web 支持属性路由 (使用 #[get]、#[post] 等宏)和函数路由 (使用 web::get() 等方法)。
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct HealthCheckResponse {
status: String ,
version: String ,
}
#[get("/health" )]
async fn health_check () -> impl Responder {
HttpResponse::Ok ().json (HealthCheckResponse {
status: "ok" .to_string (),
version: "1.0.0" .to_string (),
})
}
#[derive(Deserialize)]
struct CreateTaskRequest {
title: String ,
description: Option <String >,
}
#[derive(Serialize)]
struct CreateTaskResponse {
id: u32 ,
title: String ,
description: Option <String >,
completed: bool ,
}
#[post("/tasks" )]
async fn create_task (req: web::Json<CreateTaskRequest>) -> impl Responder {
HttpResponse::Ok ().json (CreateTaskResponse {
id: 1 ,
title: req.title.clone (),
description: req.description.clone (),
completed: false ,
})
}
#[derive(Deserialize)]
struct GetTasksQuery {
page: Option <u32 >,
per_page: Option <u32 >,
}
#[get("/tasks" )]
async fn get_tasks (query: web::Query<GetTasksQuery>) -> impl Responder {
let page = query.page.unwrap_or (1 );
let per_page = query.per_page.unwrap_or (10 );
HttpResponse::Ok ().json (serde_json::json!({
"page" : page,
"per_page" : per_page,
"total" : 100 ,
"data" : []
}))
}
#[get("/tasks/{id}" )]
async fn get_task (path: web::Path<u32 >) -> impl Responder {
let id = path.into_inner ();
HttpResponse::Ok ().json (serde_json::json!({
"id" : id,
"title" : "学习 Rust Web 开发" ,
"description" : "深入学习 Actix-web 框架" ,
"completed" : false
}))
}
#[actix_web::main]
async fn main () -> std::io::Result <()> {
HttpServer::new (|| {
App::new ()
.service (health_check)
.service (create_task)
.service (get_tasks)
.service (get_task)
})
.bind ("127.0.0.1:8080" )?
.run ()
.await
}
3.3 请求参数解析
web::Query :解析 URL 查询参数(如 ?page=1&per_page=10)。
web::Path :解析 URL 路径参数(如 /tasks/{id})。
web::Json :解析 HTTP 请求体的 JSON 数据。
web::Form :解析 HTTP 请求体的表单数据(x-www-form-urlencoded)。
3.4 响应构建
HttpResponse::Ok().json(T) :返回 JSON 响应。
HttpResponse::Ok().body(String) :返回纯文本响应。
HttpResponse::Ok().file(String) :返回静态文件。
HttpResponse::NotFound() :返回 404 NotFound 响应。
HttpResponse::InternalServerError() :返回 500 Internal Server Error 响应。
四、中间件开发与使用
4.1 内置中间件的配置
4.1.1 日志中间件(Logging Middleware) Actix-web 的内置日志中间件可以记录 HTTP 请求的详细信息(如请求方法、URL、响应状态码、请求耗时)。
use actix_web::{App, HttpServer, middleware::Logger};
use env_logger::Env;
#[actix_web::main]
async fn main () -> std::io::Result <()> {
env_logger::Builder::from_env (Env::default ().default_filter_or ("info" )).init ();
HttpServer::new (|| {
App::new ()
.wrap (Logger::default ())
.service (health_check)
.service (create_task)
.service (get_tasks)
.service (get_task)
})
.bind ("127.0.0.1:8080" )?
.run ()
.await
}
4.1.2 CORS 中间件(CORS Middleware) CORS 中间件用于允许跨域请求(Cross-Origin Resource Sharing)。
use actix_web::{App, HttpServer, middleware::Logger};
use actix_cors::Cors;
#[actix_web::main]
async fn main () -> std::io::Result <()> {
HttpServer::new (|| {
App::new ()
.wrap (Logger::default ())
.wrap (
Cors::default ()
.allow_any_origin ()
.allow_any_method ()
.allow_any_header ()
.max_age (3600 ),
)
.service (health_check)
.service (create_task)
.service (get_tasks)
.service (get_task)
})
.bind ("127.0.0.1:8080" )?
.run ()
.await
}
⚠️ 生产环境注意事项 :在生产环境中,应该配置 allow_origin 为允许的域名,而不是 allow_any_origin(),以提高安全性。
4.2 自定义中间件的实现 自定义中间件需要实现 actix_web::dev::Service trait。
use actix_web::{
dev::{ServiceRequest, ServiceResponse},
Error, HttpResponse,
};
use actix_web::middleware::Logger;
use actix_web::web;
use actix_web::http::header::AUTHORIZATION;
use jsonwebtoken::{decode, encode, Header, Validation};
use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant};
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String ,
exp: usize ,
iat: usize ,
}
struct JwtConfig {
secret_key: String ,
expiration: Duration,
}
impl Default for JwtConfig {
fn default () -> Self {
JwtConfig {
secret_key: "your-secret-key" .to_string (),
expiration: Duration::from_secs (3600 ),
}
}
}
impl JwtConfig {
fn encode (&self , sub: &str ) -> Result <String , jsonwebtoken::errors::Error> {
let now = Instant::now ();
let claims = Claims {
sub: sub.to_string (),
exp: (now + self .expiration).as_secs () as usize ,
iat: now.as_secs () as usize ,
};
encode (&Header::default (), &claims, self .secret_key.as_bytes ())
}
fn decode (&self , token: &str ) -> Result <Claims, jsonwebtoken::errors::Error> {
decode (token, self .secret_key.as_bytes (), &Validation::default ()).map (|data| data.claims)
}
}
struct JwtMiddleware {
config: JwtConfig,
}
impl JwtMiddleware {
fn new (config: JwtConfig) -> Self {
JwtMiddleware { config }
}
async fn authenticate (&self , req: &ServiceRequest) -> Result <(), Error> {
let auth_header = req.headers ().get (AUTHORIZATION);
if auth_header.is_none () {
return Err (actix_web::error::ErrorUnauthorized ("缺少 Authorization 头" ));
}
let auth_value = auth_header
.unwrap ()
.to_str ()
.map_err (|_| actix_web::error::ErrorUnauthorized ("无效的 Authorization 头" ))?;
if !auth_value.starts_with ("Bearer " ) {
return Err (actix_web::error::ErrorUnauthorized ("无效的授权类型" ));
}
let token = auth_value
.strip_prefix ("Bearer " )
.ok_or (actix_web::error::ErrorUnauthorized ("无效的 Bearer 令牌" ))?;
match self .config.decode (token) {
Ok (claims) => {
req.extensions_mut ().insert (claims);
Ok (())
}
Err (e) => {
match e.kind () {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
Err (actix_web::error::ErrorUnauthorized ("令牌已过期" ))
}
_ => Err (actix_web::error::ErrorUnauthorized (format! ("无效的令牌:{}" , e))),
}
}
}
}
}
impl <S, B> actix_web::dev::Service<ServiceRequest> for JwtMiddleware
where
S: actix_web::dev::Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static ,
B: 'static ,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = std::pin::Pin<Box <dyn futures::Future<Output = Result <Self ::Response, Self ::Error>>>>;
actix_web::dev::forward_ready!(service);
fn call (&self , req: ServiceRequest) -> Self ::Future {
let svc = self .service.clone ();
let config = self .config.clone ();
Box ::pin (async move {
match config.authenticate (&req).await {
Ok (()) => svc.call (req).await ,
Err (e) => Ok (ServiceResponse::new (
req.into_parts ().0 ,
HttpResponse::Unauthorized ().json (serde_json::json!({"error" : e.to_string ()})),
)),
}
})
}
}
fn jwt_middleware (config: JwtConfig) -> impl actix_web ::dev::Transform<
actix_web::dev::Service<ServiceRequest>,
Response = ServiceResponse,
Error = Error,
> {
actix_web::middleware::from_fn (move |req, srv| {
let config = config.clone ();
async move {
match config.authenticate (&req).await {
Ok (()) => srv.call (req).await ,
Err (e) => Ok (ServiceResponse::new (
req.into_parts ().0 ,
HttpResponse::Unauthorized ().json (serde_json::json!({"error" : e.to_string ()})),
)),
}
}
})
}
五、数据库操作与集成
5.1 数据库连接池配置 使用 SQLx 的连接池可以避免频繁地创建和销毁数据库连接,提高性能。
use actix_web::{App, HttpServer};
use sqlx::postgres::PgPoolOptions;
use std::env;
#[actix_web::main]
async fn main () -> std::io::Result <()> {
let database_url = env::var ("DATABASE_URL" ).expect ("DATABASE_URL 环境变量未设置" );
let pool = PgPoolOptions::new ()
.max_connections (5 )
.connect (&database_url)
.await
.expect ("无法连接到数据库" );
HttpServer::new (move || {
App::new ()
.app_data (web::Data::new (pool.clone ()))
.wrap (jwt_middleware (JwtConfig::default ()))
.service (health_check)
.service (create_task)
.service (get_tasks)
.service (get_task)
})
.bind ("127.0.0.1:8080" )?
.run ()
.await
}
5.2 类型安全的 SQL 查询 使用 SQLx 的查询宏可以实现类型安全的 SQL 查询,避免 SQL 注入。
use actix_web::{web, HttpResponse, Responder};
use sqlx::PgPool;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
struct Task {
id: i32 ,
title: String ,
description: Option <String >,
completed: bool ,
created_at: chrono::DateTime<chrono::Utc>,
updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Deserialize)]
struct CreateTaskRequest {
title: String ,
description: Option <String >,
}
#[post("/tasks" )]
async fn create_task (
req: web::Json<CreateTaskRequest>,
pool: web::Data<PgPool>,
) -> impl Responder {
let task = sqlx::query_as!(
Task,
r#" INSERT INTO tasks (title, description) VALUES ($1, $2) RETURNING id, title, description, completed, created_at, updated_at "# ,
req.title,
req.description
)
.fetch_one (pool.get_ref ())
.await ;
match task {
Ok (task) => HttpResponse::Ok ().json (task),
Err (e) => HttpResponse::InternalServerError ().json (serde_json::json!({"error" : e.to_string ()})),
}
}
#[derive(Debug, Deserialize)]
struct GetTasksQuery {
page: Option <i32 >,
per_page: Option <i32 >,
}
#[get("/tasks" )]
async fn get_tasks (
query: web::Query<GetTasksQuery>,
pool: web::Data<PgPool>,
) -> impl Responder {
let page = query.page.unwrap_or (1 );
let per_page = query.per_page.unwrap_or (10 );
let offset = (page - 1 ) * per_page;
let tasks = sqlx::query_as!(
Task,
r#" SELECT id, title, description, completed, created_at, updated_at FROM tasks ORDER BY created_at DESC LIMIT $1 OFFSET $2 "# ,
per_page as i64 ,
offset as i64
)
.fetch_all (pool.get_ref ())
.await ;
let total = sqlx::query_scalar!(r#" SELECT COUNT(*) FROM tasks "# )
.fetch_one (pool.get_ref ())
.await ;
match (tasks, total) {
(Ok (tasks), Ok (total)) => HttpResponse::Ok ().json (serde_json::json!({
"page" : page,
"per_page" : per_page,
"total" : total,
"data" : tasks
})),
(Err (e), _) => HttpResponse::InternalServerError ().json (serde_json::json!({"error" : e.to_string ()})),
(_, Err (e)) => HttpResponse::InternalServerError ().json (serde_json::json!({"error" : e.to_string ()})),
}
}
六、真实案例应用
6.1 任务管理系统 API
6.1.1 数据库表结构设计 首先,我们需要设计任务管理系统的数据库表结构。我们将使用 PostgreSQL 数据库,创建以下两个表:
users 表 :存储用户信息(用户名、密码哈希)。
tasks 表 :存储任务信息(标题、描述、完成状态、创建时间、更新时间、用户 ID)。
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY ,
username VARCHAR (50 ) NOT NULL UNIQUE ,
password_hash VARCHAR (255 ) NOT NULL ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY ,
title VARCHAR (100 ) NOT NULL ,
description TEXT,
completed BOOLEAN NOT NULL DEFAULT FALSE ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_tasks_user_id ON tasks(user_id);
CREATE INDEX IF NOT EXISTS idx_tasks_completed ON tasks(completed);
6.1.2 项目目录结构 task-management-api/
├── Cargo.toml
├── Cargo.lock
├── .env
├── .env.example
├── src/
│ ├── main.rs
│ ├── models.rs
│ ├── routes/
│ │ ├── mod .rs
│ │ ├── auth.rs
│ │ └── tasks.rs
│ ├── middleware/
│ │ ├── mod .rs
│ │ └── jwt.rs
│ ├── utils/
│ │ ├── mod .rs
│ │ ├── hash.rs
│ │ └── jwt.rs
│ └── database.rs
└── migrations/
└── 0001 _initial.sql
6.1.3 核心代码实现 use actix_web::{App, HttpServer};
use dotenv::dotenv;
use std::env;
mod models;
mod routes;
mod middleware;
mod utils;
mod database;
#[actix_web::main]
async fn main () -> std::io::Result <()> {
dotenv ().ok ();
let database_url = env::var ("DATABASE_URL" ).expect ("DATABASE_URL 环境变量未设置" );
let pool = database::connect (&database_url).await ;
HttpServer::new (move || {
App::new ()
.app_data (pool.clone ())
.configure (routes::config)
})
.bind ("127.0.0.1:8080" )?
.run ()
.await
}
⌨️ routes/auth.rs (用户认证路由):
use actix_web::{web, HttpResponse, Responder};
use argon2::password_hash::SaltString;
use argon2::Argon2;
use rand::thread_rng;
use rand::distributions::Alphanumeric;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use crate::models::User;
use crate::utils::jwt;
#[derive(Debug, Deserialize)]
struct RegisterRequest {
username: String ,
password: String ,
}
#[derive(Debug, Serialize)]
struct RegisterResponse {
id: i32 ,
username: String ,
created_at: chrono::DateTime<chrono::Utc>,
}
#[post("/auth/register" )]
async fn register (
req: web::Json<RegisterRequest>,
pool: web::Data<PgPool>,
) -> impl Responder {
let salt = SaltString::generate (&mut thread_rng ());
let password_hash = Argon2::default ()
.hash_password (req.password.as_bytes (), &salt)
.expect ("密码哈希失败" )
.to_string ();
let user = sqlx::query_as!(
User,
r#" INSERT INTO users (username, password_hash) VALUES ($1, $2) RETURNING id, username, password_hash, created_at, updated_at "# ,
req.username,
password_hash
)
.fetch_one (pool.get_ref ())
.await ;
match user {
Ok (user) => HttpResponse::Ok ().json (RegisterResponse {
id: user.id,
username: user.username,
created_at: user.created_at,
}),
Err (e) => HttpResponse::InternalServerError ().json (serde_json::json!({"error" : e.to_string ()})),
}
}
#[derive(Debug, Deserialize)]
struct LoginRequest {
username: String ,
password: String ,
}
#[derive(Debug, Serialize)]
struct LoginResponse {
access_token: String ,
token_type: String ,
expires_in: u32 ,
}
#[post("/auth/login" )]
async fn login (
req: web::Json<LoginRequest>,
pool: web::Data<PgPool>,
) -> impl Responder {
let user = sqlx::query_as!(
User,
r#" SELECT id, username, password_hash, created_at, updated_at FROM users WHERE username = $1 "# ,
req.username
)
.fetch_one (pool.get_ref ())
.await ;
match user {
Ok (user) => {
let password_hash = argon2::PasswordHash::new (&user.password_hash).expect ("密码哈希格式错误" );
if argon2::verify_password (req.password.as_bytes (), &password_hash).is_ok () {
let access_token = jwt::encode (&user.username);
HttpResponse::Ok ().json (LoginResponse {
access_token,
token_type: "Bearer" .to_string (),
expires_in: 3600 ,
})
} else {
HttpResponse::Unauthorized ().json (serde_json::json!({"error" : "用户名或密码错误" }))
}
}
Err (e) => HttpResponse::Unauthorized ().json (serde_json::json!({"error" : "用户名或密码错误" })),
}
}
pub fn config (cfg: &mut web::ServiceConfig) {
cfg.service (register).service (login);
}
⌨️ routes/tasks.rs (任务管理路由):
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use crate::models::Task;
use crate::utils::jwt;
#[derive(Debug, Deserialize)]
struct CreateTaskRequest {
title: String ,
description: Option <String >,
}
#[derive(Debug, Deserialize)]
struct UpdateTaskRequest {
title: Option <String >,
description: Option <String >,
completed: Option <bool >,
}
#[derive(Debug, Deserialize)]
struct GetTasksQuery {
page: Option <i32 >,
per_page: Option <i32 >,
}
#[post("/tasks" )]
async fn create_task (
req: web::Json<CreateTaskRequest>,
pool: web::Data<PgPool>,
claims: web::ReqData<jwt::Claims>,
) -> impl Responder {
let task = sqlx::query_as!(
Task,
r#" INSERT INTO tasks (title, description, user_id) VALUES ($1, $2, (SELECT id FROM users WHERE username = $3)) RETURNING id, title, description, completed, created_at, updated_at, user_id "# ,
req.title,
req.description,
claims.sub
)
.fetch_one (pool.get_ref ())
.await ;
match task {
Ok (task) => HttpResponse::Ok ().json (task),
Err (e) => HttpResponse::InternalServerError ().json (serde_json::json!({"error" : e.to_string ()})),
}
}
#[get("/tasks" )]
async fn get_tasks (
query: web::Query<GetTasksQuery>,
pool: web::Data<PgPool>,
claims: web::ReqData<jwt::Claims>,
) -> impl Responder {
let page = query.page.unwrap_or (1 );
let per_page = query.per_page.unwrap_or (10 );
let offset = (page - 1 ) * per_page;
let tasks = sqlx::query_as!(
Task,
r#" SELECT id, title, description, completed, created_at, updated_at, user_id FROM tasks WHERE user_id = (SELECT id FROM users WHERE username = $1) ORDER BY created_at DESC LIMIT $2 OFFSET $3 "# ,
claims.sub,
per_page as i64 ,
offset as i64
)
.fetch_all (pool.get_ref ())
.await ;
let total = sqlx::query_scalar!(
r#" SELECT COUNT(*) FROM tasks WHERE user_id = (SELECT id FROM users WHERE username = $1) "# ,
claims.sub
)
.fetch_one (pool.get_ref ())
.await ;
match (tasks, total) {
(Ok (tasks), Ok (total)) => HttpResponse::Ok ().json (serde_json::json!({
"page" : page,
"per_page" : per_page,
"total" : total,
"data" : tasks
})),
(Err (e), _) => HttpResponse::InternalServerError ().json (serde_json::json!({"error" : e.to_string ()})),
(_, Err (e)) => HttpResponse::InternalServerError ().json (serde_json::json!({"error" : e.to_string ()})),
}
}
#[get("/tasks/{id}" )]
async fn get_task (
path: web::Path<i32 >,
pool: web::Data<PgPool>,
claims: web::ReqData<jwt::Claims>,
) -> impl Responder {
let id = path.into_inner ();
let task = sqlx::query_as!(
Task,
r#" SELECT id, title, description, completed, created_at, updated_at, user_id FROM tasks WHERE id = $1 AND user_id = (SELECT id FROM users WHERE username = $2) "# ,
id,
claims.sub
)
.fetch_one (pool.get_ref ())
.await ;
match task {
Ok (task) => HttpResponse::Ok ().json (task),
Err (e) => HttpResponse::NotFound ().json (serde_json::json!({"error" : "任务不存在" })),
}
}
#[put("/tasks/{id}" )]
async fn update_task (
path: web::Path<i32 >,
req: web::Json<UpdateTaskRequest>,
pool: web::Data<PgPool>,
claims: web::ReqData<jwt::Claims>,
) -> impl Responder {
let id = path.into_inner ();
let task = sqlx::query_as!(
Task,
r#" UPDATE tasks SET title = COALESCE($1, title), description = COALESCE($2, description), completed = COALESCE($3, completed), updated_at = NOW() WHERE id = $4 AND user_id = (SELECT id FROM users WHERE username = $5) RETURNING id, title, description, completed, created_at, updated_at, user_id "# ,
req.title,
req.description,
req.completed,
id,
claims.sub
)
.fetch_one (pool.get_ref ())
.await ;
match task {
Ok (task) => HttpResponse::Ok ().json (task),
Err (e) => HttpResponse::NotFound ().json (serde_json::json!({"error" : "任务不存在" })),
}
}
#[delete("/tasks/{id}" )]
async fn delete_task (
path: web::Path<i32 >,
pool: web::Data<PgPool>,
claims: web::ReqData<jwt::Claims>,
) -> impl Responder {
let id = path.into_inner ();
let task = sqlx::query_as!(
Task,
r#" DELETE FROM tasks WHERE id = $1 AND user_id = (SELECT id FROM users WHERE username = $2) RETURNING id, title, description, completed, created_at, updated_at, user_id "# ,
id,
claims.sub
)
.fetch_one (pool.get_ref ())
.await ;
match task {
Ok (task) => HttpResponse::Ok ().json (task),
Err (e) => HttpResponse::NotFound ().json (serde_json::json!({"error" : "任务不存在" })),
}
}
pub fn config (cfg: &mut web::ServiceConfig) {
cfg.service (create_task)
.service (get_tasks)
.service (get_task)
.service (update_task)
.service (delete_task);
}
七、部署与监控
7.1 Docker 容器化部署 # 使用官方的 Rust 镜像作为基础镜像
FROM rust:1.75-slim-bullseye AS builder
# 安装构建依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
libssl-dev \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
# 设置工作目录
WORKDIR /app
# 复制 Cargo.toml 和 Cargo.lock
COPY Cargo.toml Cargo.lock ./
# 创建一个虚拟库来缓存依赖
RUN mkdir -p src && echo 'fn main() {}' > src/main.rs
RUN cargo build --release
# 复制源代码
COPY . .
# 重新构建项目
RUN cargo build --release
# 使用 Debian Bullseye 作为基础镜像
FROM debian:bullseye-slim
# 安装运行依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
libssl1.1 \
&& rm -rf /var/lib/apt/lists/*
# 复制二进制文件
COPY --from=builder /app/target/release/task-management-api /usr/local/bin/
# 暴露端口
EXPOSE 8080
# 入口点
CMD ["task-management-api"]
version: "3.8"
services:
task-management-api:
build: .
ports:
- "8080:8080"
environment:
DATABASE_URL: postgres://postgres:password@postgres:5432/task_management
depends_on:
- postgres
restart: always
postgres:
image: postgres:15-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: task_management
volumes:
- postgres_data:/var/lib/postgresql/data
- ./migrations:/docker-entrypoint-initdb.d
restart: always
volumes:
postgres_data:
7.2 Prometheus+Grafana 监控 global:
scrape_interval: 15s
scrape_configs:
- job_name: "task-management-api"
static_configs:
- targets: ["localhost:8080" ]
② 编写 docker-compose.yml(包含 Prometheus 和 Grafana):
version: "3.8"
services:
task-management-api:
build: .
ports:
- "8080:8080"
environment:
DATABASE_URL: postgres://postgres:password@postgres:5432/task_management
depends_on:
- postgres
restart: always
postgres:
image: postgres:15-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: task_management
volumes:
- postgres_data:/var/lib/postgresql/data
- ./migrations:/docker-entrypoint-initdb.d
restart: always
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
restart: always
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
restart: always
volumes:
postgres_data:
grafana_data:
八、常见问题与解决方案
8.1 CORS 跨域配置错误 问题现象 :前端无法发送请求,浏览器控制台提示'Access-Control-Allow-Origin' header 缺失。
检查 CORS 中间件的配置是否正确。
确保 allow_origin 配置为允许的域名。
如果是开发环境,可以使用 allow_any_origin()。
8.2 数据库连接池配置不当 问题现象 :高并发下服务器响应时间变慢,甚至返回 500 Internal Server Error。
调整连接池大小,通常为 CPU 核心数的 2-4 倍。
监控数据库连接池的使用情况。
避免长时间占用数据库连接。
8.3 JWT 令牌过期未处理
实现 JWT 的刷新机制,使用刷新令牌获取新的访问令牌。
调整访问令牌的过期时间。
在前端实现令牌自动刷新。
九、总结与展望
9.1 总结 ✅ 框架选型对比 :深入了解了 Actix-web、Rocket、Axum 三大主流 Rust Web 框架的架构差异、性能表现与适用场景。
✅ Actix-web 核心用法 :熟练掌握了项目初始化、路由定义、请求参数解析、响应构建。
✅ 中间件开发与使用 :学习了内置中间件(日志、CORS)的配置,以及自定义 JWT 认证中间件的实现方法。
✅ 数据库操作与集成 :使用 SQLx 异步连接 PostgreSQL,实现了类型安全的 SQL 查询、事务管理。
✅ 完整 API 构建实战 :结合真实场景编写了任务管理系统 API,包含用户认证(JWT)、任务 CRUD、分页查询等功能。
✅ 部署与监控 :掌握了 Docker 容器化部署,以及 Prometheus+Grafana 监控的配置。
9.2 展望 下一篇文章,我们将深入学习 Rust 的微服务架构 ,包括使用 gRPC 通信、使用 Consul 进行服务发现、使用 Docker Swarm 或 Kubernetes 进行容器编排,通过这些知识我们将能够构建可伸缩、高可用的微服务系统。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online