Actix-Web完整项目实战:博客 API

一、概述

在前面几篇文章中,已经熟悉了 Actix-Web框架,各个组件。接下来实现一个博客 API,基于RESTful API风格,集成token验证。

Actix-Web完整项目实战:博客 API

 

二、项目结构

代码结构

新建一个项目blog-api,代码结构如下:

./ ├── Cargo.toml └── src     ├── db.rs     ├── handlers     │   ├── dev.rs     │   ├── mod.rs     │   ├── post_handler.rs     │   └── user_handler.rs     ├── jwt.rs     ├── main.rs     ├── middleware     │   ├── auth.rs     │   └── mod.rs     └── models         ├── mod.rs         ├── post.rs         └── user.rs

 

表结构

数据库在阿里云上面,创建一个测试数据库

CREATE DATABASE rust_blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

新建表users

CREATE TABLE `users` (   `id` bigint NOT NULL AUTO_INCREMENT,   `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,   `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,   `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,   `create_time` datetime DEFAULT CURRENT_TIMESTAMP,   PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

新建表posts

CREATE TABLE `posts` (   `id` bigint NOT NULL AUTO_INCREMENT,   `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,   `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,   `author_id` bigint NOT NULL,   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,   `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,   PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

依赖组件

Cargo.toml

[package] name = "actix_swagger" version = "0.1.0" edition = "2024"  [dependencies] actix-web = { version = "4.12", features = ["compress-gzip"] } tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = "1.0" utoipa = { version = "5", features = ["actix_extras", "chrono"] } utoipa-swagger-ui = { version = "9", features = ["actix-web"] } log = "0.4"      # 日志门面 env_logger = "0.11"     # 控制台实现 # 异步 MySQL 驱动,支持 8.x chrono = { version = "0.4", default-features = false, features = ["serde", "clock"] } sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "mysql", "chrono"] } dotenvy = "0.15"   # 读取 .env futures-util = { version = "0.3", default-features = false, features = ["std"] } actix-cors = "0.7" md5 = "0.8"        # 轻量、零配置 jsonwebtoken = { version = "10.2", features = ["rust_crypto"] }

 

数据模型

src/models/user.rs

use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; use sqlx::FromRow;   #[derive(Debug, Serialize, Deserialize, FromRow, utoipa::ToSchema)] pub struct User {     pub id: i64,     pub username: String,               // NOT NULL     #[serde(skip)]     #[allow(dead_code)]     pub password: String,               // NOT NULL     pub email: Option<String>,          // NULL  -> Option     pub create_time: Option<NaiveDateTime>, // NULL -> Option }   #[derive(Debug, Deserialize, utoipa::ToSchema)] pub struct CreateUser {     pub username: String,     pub email: String,     pub password: String, }

 

src/models/post.rs

use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; use sqlx::FromRow;  #[derive(Debug, Serialize, Deserialize, FromRow, utoipa::ToSchema)] pub struct Post {     pub id: i64,     pub title: String,     pub content: String,     pub author_id: i64,     pub create_time: NaiveDateTime,     pub update_time: Option<NaiveDateTime>, // 允许 NULL }  #[derive(Debug, Deserialize, utoipa::ToSchema)] pub struct CreatePost {     pub title: String,     pub content: String, }

 

数据库操作

src/db.rs

use sqlx::{mysql::MySqlPool}; use crate::models::{User, CreateUser, Post, CreatePost}; use md5; pub async fn create_user(     pool: &MySqlPool,     user: CreateUser, ) -> Result<User, sqlx::Error> {     let mut tx = pool.begin().await?;      // 1. 插入     //计算 MD5(16 进制小写)     let password_hash = format!("{:x}", md5::compute(&user.password));     sqlx::query!(         r#"         INSERT INTO users (username, password, email, create_time)         VALUES (?, ?, ?, NOW())         "#,         user.username,         password_hash,          // 已加密         user.email,     )     .execute(&mut *tx)     .await?;      // 2. LAST_INSERT_ID() 返回 u64,不需要 unwrap_or     let id: u64 = sqlx::query_scalar!("SELECT LAST_INSERT_ID()")         .fetch_one(&mut *tx)         .await?;      // 3. 查新行 —— 明确列 NULL 性,与 User 结构体对应     let user = sqlx::query_as!(         User,         "SELECT id, username, password, email, create_time FROM users WHERE id = ?",         id as i64     )     .fetch_one(&mut *tx)     .await?;      tx.commit().await?;     Ok(user) }   pub async fn get_user_by_id(pool: &MySqlPool, id: i64) -> Result<Option<User>,sqlx::Error> {     let user = sqlx::query_as::<_, User>(         "SELECT id, username, email, create_time, '' as password FROM users WHERE id = ?"     )     .bind(id)     .fetch_optional(pool)     .await?;          Ok(user) }   pub async fn create_post(     pool: &MySqlPool,     post: CreatePost,     author_id: i64, ) -> Result<Post, sqlx::Error> {     let mut tx = pool.begin().await?;      // 1. 插入     sqlx::query!(         r#"         INSERT INTO posts (title, content, author_id, create_time, update_time)         VALUES (?, ?, ?, NOW(), NULL)         "#,         post.title,         post.content,         author_id     )     .execute(&mut *tx)     .await?;      // 2. 取新 id     let id: u64 = sqlx::query_scalar!("SELECT LAST_INSERT_ID()")         .fetch_one(&mut *tx)         .await?;      // 3. 再查整行     let new_post = sqlx::query_as!(         Post,         "SELECT id, title, content, author_id, create_time, update_time           FROM posts WHERE id = ?",         id as i64     )     .fetch_one(&mut *tx)     .await?;      tx.commit().await?;     Ok(new_post) }   pub async fn get_posts(pool: &MySqlPool, limit: i64) -> Result<Vec<Post>,sqlx::Error> {     let posts = sqlx::query_as::<_, Post>(         "SELECT id, title, content, author_id, create_time, update_time           FROM posts           ORDER BY create_time DESC           LIMIT ?"     )     .bind(limit)     .fetch_all(pool)     .await?;          Ok(posts) }  pub async fn get_users(pool: &MySqlPool, limit: i64) -> Result<Vec<User>,sqlx::Error> {     let users = sqlx::query_as::<_, User>(         "SELECT id, username, '' as password, email, create_time           FROM users           ORDER BY create_time DESC           LIMIT ?"     )     .bind(limit)     .fetch_all(pool)     .await?;          Ok(users) }  /// 根据ID获取单个帖子 pub async fn get_post_by_id(pool: &MySqlPool, id: i64) -> Result<Option<Post>,sqlx::Error> {     let post = sqlx::query_as::<_, Post>(         "SELECT id, title, content, author_id, create_time, update_time           FROM posts WHERE id = ?"     )     .bind(id)     .fetch_optional(pool)     .await?;          Ok(post) }

 

环境变量

.env

DATABASE_URL=mysql://root:123456@localhost:3306/rust_blog

注意:如果密码带有@符号,需要进行URL-encode编码

打开在线url编码器,链接:https://www.convertstring.com/zh_CN/EncodeDecode/UrlEncode

请求处理器

 src/handlers/user_handler.rs

use actix_web::{web, HttpResponse, Result}; use sqlx::{mysql::MySqlPool}; use crate::{db, models::CreateUser}; use crate::models::User;  // 用户相关API接口模块 // 提供用户创建和查询功能  /// 创建新用户接口 ///  /// 用于注册新用户,创建用户账号并返回用户信息 #[utoipa::path(     post,     path = "/api/users",     request_body = CreateUser,     description = "注册新用户账号",     summary = "创建用户",     responses(         (status = 200, description = "用户创建成功", body = User),         (status = 400, description = "请求参数格式错误或用户已存在"),         (status = 500, description = "服务器内部错误")     ),     tag = "用户管理" )] pub async fn create_user_handler(     pool: web::Data<MySqlPool>,     user: web::Json<CreateUser>, ) -> Result<HttpResponse> {     match db::create_user(pool.get_ref(), user.into_inner()).await {         Ok(user) => Ok(HttpResponse::Created().json(user)),         Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({             "error": e.to_string()         })))     } }  /// 根据ID获取用户信息接口 ///  /// 通过用户ID查询特定用户的详细信息 #[utoipa::path(     get,     path = "/api/users/{id}",     description = "根据用户ID获取用户详细信息",     summary = "查询用户详情",     responses(         (status = 200, description = "查询成功", body = User),         (status = 404, description = "用户不存在"),         (status = 500, description = "服务器内部错误")     ),     params(         ("id" = i64, Path, description = "用户ID,用于唯一标识用户")     ),     tag = "用户管理" )]  pub async fn get_user_handler(     pool: web::Data<MySqlPool>,     user_id: web::Path<i64>, ) -> Result<HttpResponse> {     match db::get_user_by_id(pool.get_ref(), user_id.into_inner()).await {         Ok(Some(user)) => Ok(HttpResponse::Ok().json(user)),         Ok(None) => Ok(HttpResponse::NotFound().json(serde_json::json!({             "error": "User not found"         }))),         Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({             "error": e.to_string()         })))     } }  /// 获取用户列表接口 ///  /// 获取系统中的用户列表,支持分页查询 #[utoipa::path(     get,     path = "/api/users",     description = "获取用户列表,支持通过limit参数限制返回数量",     summary = "查询用户列表",     responses(         (status = 200, description = "查询成功", body = [User]),         (status = 500, description = "服务器内部错误")     ),     params(         ("limit" = Option<i64>, Query, description = "限制返回数量,默认为10"),         ("page" = Option<i64>, Query, description = "页码,从1开始")     ),     tag = "用户管理" )] pub async fn get_users_handler(     pool: web::Data<MySqlPool>,     query: web::Query<std::collections::HashMap<String, String>>, ) -> Result<HttpResponse> {     let limit = query.get("limit")         .and_then(|s| s.parse().ok())         .unwrap_or(10);          match db::get_users(pool.get_ref(), limit).await {         Ok(users) => Ok(HttpResponse::Ok().json(users)),         Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({             "error": e.to_string()         })))     } }

 

 src/handlers/post_handler.rs

use sqlx::{mysql::MySqlPool}; use crate::models::{CreatePost, Post};   // 模型 use crate::db;                           // 数据库函数 use std::collections::HashMap;           // HashMap use actix_web::{web, HttpRequest, HttpMessage,HttpResponse}; // 解决 extensions() 不可见  // 帖子相关API接口模块 // 提供帖子创建和查询功能  /// 创建新帖子接口 ///  /// 允许认证用户创建新的帖子内容,帖子将与当前认证用户关联 #[utoipa::path(     post,     path = "/api/posts",     request_body = CreatePost,     description = "创建新的博客帖子,需要用户认证",     summary = "创建帖子",     responses(         (status = 200, description = "帖子创建成功", body = Post),         (status = 400, description = "请求参数格式错误"),         (status = 404, description = "未找到相关资源"),         (status = 500, description = "服务器内部错误")     ),     tag = "帖子管理" )] pub async fn create_post_handler(     pool: web::Data<MySqlPool>,     post: web::Json<CreatePost>,     req: HttpRequest, ) -> Result<HttpResponse, actix_web::Error> {     // 从请求扩展中获取认证的用户ID     let author_id = req.extensions().get::<i64>().copied().unwrap_or(1);     println!("author_id: {}", author_id);          match db::create_post(pool.get_ref(), post.into_inner(), author_id).await {         Ok(post) => Ok(HttpResponse::Created().json(post)),         Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({             "error": e.to_string()         })))     } }   /// 获取帖子列表接口 ///  /// 获取系统中的帖子列表,支持分页查询 #[utoipa::path(     get,     path = "/api/posts",     description = "获取帖子列表,支持通过limit参数限制返回数量",     summary = "查询帖子列表",     responses(         (status = 200, description = "查询成功", body = [Post]),         (status = 500, description = "服务器内部错误")     ),     params(         ("limit" = Option<i64>, Query, description = "限制返回数量,默认为10"),         ("page" = Option<i64>, Query, description = "页码,从1开始")     ),     tag = "帖子管理" )] pub async fn get_posts_handler(     pool: web::Data<MySqlPool>,     query: web::Query<HashMap<String, String>>, ) -> Result<HttpResponse, actix_web::Error> {     let limit = query.get("limit")         .and_then(|s| s.parse().ok())         .unwrap_or(10);          match db::get_posts(pool.get_ref(), limit).await {          Ok(posts) => Ok(HttpResponse::Ok().json(posts)),         Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({             "error": e.to_string()         })))     } }  /// 获取帖子详情接口 ///  /// 根据帖子ID获取单个帖子的详细信息 #[utoipa::path(     get,     path = "/api/posts/{id}",     description = "根据ID获取单个帖子的详细信息",     summary = "查询帖子详情",     responses(         (status = 200, description = "查询成功", body = Post),         (status = 404, description = "帖子不存在"),         (status = 500, description = "服务器内部错误")     ),     params(         ("id" = i64, Path, description = "帖子ID", example = 1)     ),     tag = "帖子管理" )] pub async fn get_post_handler(     pool: web::Data<MySqlPool>,     id: web::Path<i64>, ) -> Result<HttpResponse, actix_web::Error> {     match db::get_post_by_id(pool.get_ref(), *id).await {         Ok(Some(post)) => Ok(HttpResponse::Ok().json(post)),         Ok(None) => Ok(HttpResponse::NotFound().json(serde_json::json!({             "error": "帖子不存在"         }))),         Err(e) => Ok(HttpResponse::InternalServerError().json(serde_json::json!({             "error": e.to_string()         })))     } }

 

src/handlers/dev.rs

use actix_web::{get, web, HttpResponse, Responder}; use serde::{Serialize}; use utoipa::{ToSchema};  #[derive(Serialize, ToSchema)] struct TokenReply {     message: String, }  // 内部函数,处理实际的token生成逻辑 fn generate_token(user_id: u32) -> String {     let user_id_i64: i64 = user_id as i64;     let t = crate::jwt::make_token(user_id_i64, 24);     format!("Bearer {}", t) }  /// 生成开发测试token(指定用户ID) ///  /// 用于开发环境测试时快速生成认证令牌,使用指定的用户ID #[utoipa::path(     get,     path = "/dev/token/{user_id}",     responses(         (status = 200, description = "成功生成测试token", body = TokenReply)     ),     tag = "开发工具" )] #[get("/token/{user_id:[0-9]+}")] pub async fn dev_token(user_id: web::Path<u32>) -> impl Responder {     let token = generate_token(*user_id);     HttpResponse::Ok().body(token) }  /// 生成开发测试token(默认用户ID=1) ///  /// 用于开发环境测试时快速生成认证令牌,使用默认用户ID=1 #[utoipa::path(     get,     path = "/dev/token",     responses(         (status = 200, description = "成功生成测试token", body = TokenReply)     ),     tag = "开发工具" )] #[get("/token")] pub async fn dev_token_default() -> impl Responder {     // 直接调用内部函数生成token,使用默认user_id=1     let token = generate_token(1);     HttpResponse::Ok().body(token) }

 

src/handlers/mod.rs

// 把子模块引进来 pub mod user_handler; pub mod post_handler; pub mod dev;    // 再导出给 main.rs 用 pub use user_handler::{create_user_handler, get_user_handler, get_users_handler}; pub use post_handler::{get_posts_handler, create_post_handler, get_post_handler};

 

中间件

src/middleware/auth.rs

use actix_web::{     dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},     Error, HttpMessage, }; use futures_util::future::LocalBoxFuture; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use crate::jwt::{Claims, SECRET};  pub struct AuthMiddleware;  pub struct AuthMiddlewareService<S> {     service: S, }  impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware where     S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,     S::Future: 'static,     B: 'static, {     type Response = ServiceResponse<B>;     type Error = Error;     type InitError = ();     type Transform = AuthMiddlewareService<S>;     type Future = std::future::Ready<Result<Self::Transform, Self::InitError>>;      fn new_transform(&self, service: S) -> Self::Future {         std::future::ready(Ok(AuthMiddlewareService { service }))     } }  impl<S, B> Service<ServiceRequest> for AuthMiddlewareService<S> where     S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,     S::Future: 'static,     B: 'static, {     type Response = ServiceResponse<B>;     type Error = Error;     type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;      forward_ready!(service);      fn call(&self, req: ServiceRequest) -> Self::Future {         let token = req             .headers()             .get("Authorization")             .and_then(|h| h.to_str().ok())             .and_then(|s| s.strip_prefix("Bearer "));          let claims = match token {             Some(t) => match decode::<Claims>(                 t,                 &DecodingKey::from_secret(SECRET),                 &Validation::new(Algorithm::HS256),             ) {                 Ok(data) => data.claims,                 Err(_) => return Box::pin(async {                     Err(actix_web::error::ErrorUnauthorized("bad token"))                 }),             },             None => return Box::pin(async {                 Err(actix_web::error::ErrorUnauthorized("missing token"))             }),         };          req.extensions_mut().insert(claims.user_id);         let fut = self.service.call(req);         Box::pin(async move { fut.await })     } }

 

src/middleware/mod.rs

pub mod auth;          // 告诉编译器去同级目录找 auth.rs

 

jwt认证

jwt.rs

use chrono::Utc; use jsonwebtoken::{encode, EncodingKey, Header};  pub const SECRET: &[u8] = b"!ChangeMe!";   // 与验证端保持一致  #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct Claims {     pub user_id: i64,     pub exp: i64,   // 过期时间(UTC 时间戳) }  /// 手动生成一个有效期为 `hours` 小时的 Token pub fn make_token(user_id: i64, hours: i64) -> String {     let exp = Utc::now()         .checked_add_signed(chrono::Duration::hours(hours))         .unwrap()         .timestamp();      let claims = Claims { user_id, exp };     encode(         &Header::default(),         &claims,         &EncodingKey::from_secret(SECRET),     )     .unwrap() }

 

主程序

main.rs

mod models; mod handlers; mod db; mod middleware;   use actix_web::{web, App, HttpServer, middleware::Logger,middleware::Compress}; use sqlx::{mysql::MySqlPoolOptions}; use env_logger::Env; use dotenvy::dotenv; mod jwt;  use middleware::auth::AuthMiddleware; use handlers::dev::{dev_token, dev_token_default}; // 导入必要的类型 use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi;  // 使用derive宏实现OpenAPI #[derive(OpenApi)] #[openapi(     info(         title = "博客 API",         description = "一个使用Actix Web框架构建的RESTful API示例,用于博客文章的管理",         version = "1.0.0"     ),     // 明确列出所有路径     paths(         handlers::user_handler::create_user_handler,         handlers::user_handler::get_user_handler,         handlers::user_handler::get_users_handler,         handlers::post_handler::get_posts_handler,         handlers::post_handler::create_post_handler,         handlers::post_handler::get_post_handler,         handlers::dev::dev_token,         handlers::dev::dev_token_default     ) )] pub struct ApiDoc;   #[actix_web::main] async fn main() -> std::io::Result<()> {     dotenv().ok();     // 加载 .env 到环境变量     // 初始化日志     env_logger::init_from_env(Env::default().default_filter_or("info"));     log::info!("Starting HTTP server on http://127.0.0.1:8080");      // 建立连接池     let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");     let pool = MySqlPoolOptions::new()         .max_connections(5)         .connect(&db_url)         .await         .expect("Failed to create MySqlPool");      HttpServer::new(move || {                  App::new()             .wrap(Logger::default())             .wrap(Compress::default())              // 1. 不需要认证的接口             .service(                 web::scope("/dev")                     .service(dev_token)                     .service(dev_token_default)             )             .app_data(web::Data::new(pool.clone()))             // 使用utoipa自带的Swagger UI             .service(                 SwaggerUi::new("/swagger-ui/{_:.*}")                     .url("/api-docs/openapi.json", ApiDoc::openapi())             )             .service(                 web::scope("/api")                     .wrap(AuthMiddleware)   // 认证只拦截 /api/*                     .service(                         web::scope("/users")                             .route("", web::post().to(handlers::create_user_handler))                             .route("", web::get().to(handlers::get_users_handler))                             .route("/{id}", web::get().to(handlers::get_user_handler))                     )                     .service(                         web::scope("/posts")                             .route("", web::get().to(handlers::get_posts_handler))                             .route("", web::post().to(handlers::create_post_handler))                             .route("/{id}", web::get().to(handlers::get_post_handler))                     )             )     })     .bind(("127.0.0.1", 8080))?     .workers(4)  // 工作线程数     .run()     .await }

 

注意:这里设置的连接池大小为5,生产环境,请根据实际情况修改。

 

三、博客 API调用

获取token

首先要获取token,user_id=1,有效期24小时

使用postman进行调用,http://localhost:8080/dev/token

Actix-Web完整项目实战:博客 API

用户管理

创建用户

调用接口:http://localhost:8080/dev/token

指定token,输入上一步返回的的token

Actix-Web完整项目实战:博客 API

 

指定Content-Type:application/json

Actix-Web完整项目实战:博客 API

 

 指定请求参数,创建用户Alice

{     "username":"Alice",     "password":"123456",     "email":"alice@example.com" }

 

选择body,输入json参数,发送请求,就可以得到返回200,说明成功了

Actix-Web完整项目实战:博客 API

 

 再创建一个用户,覆盖请求参数,再次发送

{     "username":"Bob",     "password":"123456",     "email":"bob@example.com" }

 

查询用户列表

访问链接http://localhost:8080/api/users,使用get请求,效果如下

Actix-Web完整项目实战:博客 API

 查询用户详情

根据用户id查询

Actix-Web完整项目实战:博客 API

 帖子管理

 创建帖子

输入json参考,请求接口:http://localhost:8080/api/posts

{     "title":"xiao1",     "content":"abcd1" }

效果如下

Actix-Web完整项目实战:博客 API

 再创建一个帖子

{     "title":"xiao2",     "content":"abcd2" }

查询帖子列表

请求接口:http://localhost:8080/api/posts

Actix-Web完整项目实战:博客 API

 查询帖子详情

请求接口:http://localhost:8080/api/posts

Actix-Web完整项目实战:博客 API

 

 

本文参考链接:https://blog.csdn.net/sinat_41617212/article/details/154069236

发表评论

评论已关闭。

相关文章

当前内容话题