一、概述
在前面几篇文章中,已经熟悉了 Actix-Web框架,各个组件。接下来实现一个博客 API,基于RESTful API风格,集成token验证。
二、项目结构
代码结构
新建一个项目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};<br>
复制代码
中间件
src/middleware/auth.rs
[code]use actix_web::{ dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, Error, HttpMessage,};use futures_util::future: ocalBoxFuture;use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};use crate::jwt::{Claims, SECRET};pub struct AuthMiddleware;pub struct AuthMiddlewareService { service: S,}impl Transform for AuthMiddlewarewhere S: Service, S::Future: 'static, B: 'static,{ type Response = ServiceResponse<B>; type Error = Error; type InitError = (); type Transform = AuthMiddlewareService; type Future = std::future::Ready; fn new_transform(&self, service: S) -> Self::Future { std::future::ready(Ok(AuthMiddlewareService { service })) }}impl Service for AuthMiddlewareServicewhere S: Service, S::Future: 'static, B: 'static,{ type Response = ServiceResponse<B>; type Error = Error; type Future = LocalBoxFuture |