rust单体web项目模板搭建

发布于:2025-06-24 ⋅ 阅读:(16) ⋅ 点赞:(0)

代码仓库

gitee

创建项目

cargo new rust-web-starter

目录结构

/src
    /handlers
        - mod.rs
        - posts.rs
        - user.rs
    /utils
        - mod.rs
        - jwt.rs
    main.rs
.env
dev.db

数据库

使用sqlx进行数据库操作, 为了方便测试,使用sqlite数据库

创建数据库表

在代码中实现


#[tokio::main]
async fn main() {
    // 加载环境变量
    let _ = dotenvy::dotenv();

    // 日志追踪器
    tracing_subscriber::fmt::init();
    info!("Starting server");

    // 数据库连接
    let database_url = std::env::var("SQLITE_DB_URL").expect("SQLITE_DB_URL not set");
    let pool = sqlx::SqlitePool::connect(&database_url)
        .await
        .expect("Error with pool connection");

    // 建表
    sqlx::query(
        r#"create table users
            (
                id       integer
                    primary key autoincrement,
                username text,
                password text,
                email    text
            );
            "#,
    )
    .execute(&pool)
    .await;
    sqlx::query(
        r#"create table posts
            (
                id         integer
                    primary key autoincrement,
                created_at datetime,
                updated_at datetime,
                deleted_at datetime,
                title      text,
                body       text
            );

            create index idx_posts_deleted_at
                on posts (deleted_at);
            "#,
    )
    .execute(&pool)
    .await; 
    
}

密码加密

找不到合适的, 用明文

创建JWT令牌

use chrono::{DateTime, Duration, Utc};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode, errors::Error};
use serde::{Deserialize, Serialize};
use tracing::{debug, error};

#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
    pub sub: String,
    pub exp: i64,
    pub iat: i64,
}

impl Claims {
    pub fn new(sub: String, exp: DateTime<Utc>) -> Self {
        Self {
            sub,
            exp: exp.timestamp(),
            iat: Utc::now().timestamp(),
        }
    }
}

// 32字节安全密钥
pub const SECRET_KEY: &[u8] = b"030c8d02eea6e5e5219096bd076c41e58e955632d59beb7d44fa18e3fbccb0bd12345678901234";

// 生成JWT
pub fn generate_token(user_id: &str) -> Result<String, Error> {
    let claims = Claims::new(user_id.to_string(), Utc::now() + Duration::hours(1));
    let token = encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(SECRET_KEY),
    )?;
    debug!("Generated token: {}", token);
    Ok(token)
}

// 验证JWT
pub fn validate_token(token: &str) -> Result<Claims, Error> {
    debug!("Received token: {}", token);
    let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
    validation.validate_exp = true;

    match decode::<Claims>(token, &DecodingKey::from_secret(SECRET_KEY), &validation) {
        Ok(data) => {
            debug!("Decoded claims: {:?}", data.claims);
            Ok(data.claims)
        }
        Err(e) => {
            error!("Token error: {:?}", e);
            Err(e)
        }
    }
}

进行测试

/// jwt方法代码

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_generate_and_validate_token() {
        // 生成 token
        let user_id = "test_user";
        let token_result = generate_token(user_id);
        
        assert!(token_result.is_ok(), "Failed to generate token");

        let token = token_result.unwrap();

        // 验证 token
        let claims_result = validate_token(&token);

        assert!(claims_result.is_ok(), "Failed to validate token");
        
        let claims = claims_result.unwrap();
        
        // 检查 claims 中的信息是否正确
        assert_eq!(claims.sub, user_id.to_string());
    }

    #[test]
    fn test_invalid_token() {
        // 提供一个无效的 token
        let invalid_token = "invalid.token.here";

        let claims_result = validate_token(invalid_token);

        assert!(claims_result.is_err(), "Expected error for invalid token");
    }
}

帖子信息表

增删改查

posts.rs
提供获取、发布、更新和删除帖子信息的 API 接口。

use axum::{
    extract::{Path, State},
    http::StatusCode,
    Json,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use sqlx_core::sqlite::SqlitePool;

#[derive(Serialize, Deserialize)]
pub struct NewPost {
    title: String,
    body: String,
}

#[derive(Serialize, Deserialize, sqlx::FromRow)]
pub struct Post {
    id: i32,
    title: String,
    body: String, 
}

pub async fn create_post(
    // 从全局路由状态获取数据库连接池
    State(pool): State<SqlitePool>,
    Json(product): Json<NewPost>,
) -> Result<Json<Value>, (StatusCode, String)> {
    let resp = sqlx::query("INSERT INTO posts (title, body) values ($1, $2)")
        // 填充占位符
        .bind(&product.title)
        .bind(&product.body)
        .execute(&pool)
        .await
        .map_err(|err| {
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Error is: {}", err),
            )
        })?;
    Ok(Json(json!(product)))
}

pub async fn get_posts(
    State(pool): State<SqlitePool>,
) -> Result<Json<Vec<Post>>, (StatusCode, String)> {
    let result = sqlx::query_as("SELECT * from posts")
        // 数据回填到结构体
        .fetch_all(&pool)
        .await
        .map_err(|err| {
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Error is: {}", err),
            )
        })?;
    Ok(Json(result))
}

pub async fn get_one_post(
    State(pool): State<SqlitePool>,
    Path(id): Path<i32>,
) -> Result<Json<Post>, (StatusCode, String)> {
    let result = sqlx::query_as("SELECT * FROM posts WHERE id = $1")
        .bind(id)
        .fetch_one(&pool)
        .await
        .map_err(|err| match err {
            sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, format!("Error is: {}", err)),
            _ => (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Error is: {}", err),
            ),
        })?;
    Ok(Json(result))
}

pub async fn delete_post(
    State(pool): State<SqlitePool>,
    Path(id): Path<i32>,
) -> Result<Json<Value>, (StatusCode, String)> {
    let result = sqlx::query("DELETE FROM posts WHERE id = $1")
        .bind(id)
        .execute(&pool)
        .await
        .map_err(|err| match err {
            sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, format!("Error is: {}", err)),
            _ => (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Error is: {}", err),
            ),
        })?;
    Ok(Json(json!({"msg": "Product deleted successfully"})))
}

pub async fn update_post(
    State(pool): State<SqlitePool>,
    Path(id): Path<i32>,
    Json(product): Json<Post>,
) -> Result<Json<Value>, (StatusCode, String)> {
    let result = sqlx::query("UPDATE posts SET title=$1, body=$2 WHERE id=$3")
        .bind(&product.title)
        .bind(&product.body)
        .bind(id)
        .execute(&pool)
        .await
        .map_err(|err| match err {
            sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, format!("Error is: {}", err)),
            _ => (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Error is: {}", err),
            ),
        })?;
    Ok(Json(json!({"msg": "Product updated successfully"})))
}

网络接口

定义路由

restful风格API:

HTTP 方法|操作类型|示例
GET|查询资源|获取用户列表 /api/users
POST|创建资源|创建新用户 /api/users
PUT|更新资源|更新指定用户 /api/users/{id}
DELETE|删除资源|删除指定用户 /api/users/{id}


#[tokio::main]
async fn main() {
    /// 前文中的数据库生成代码

    // 网络接口代码
    let postRouter = Router::new()
        .route("/posts", get(handlers::posts::get_posts))
        .route("/posts/:id", get(handlers::posts::get_one_post))
        .route("/posts", post(handlers::posts::create_post))
        .route("/posts/:id", patch(handlers::posts::update_post))
        .route("/posts/:id", delete(handlers::posts::delete_post))
        .route_layer(middleware::from_fn(auth));

    let userRouter = Router::new()
        .route("/users", post(handlers::uesr::register))
        .route("/auth/login", post(handlers::uesr::login));

    let userProfileRouter = Router::new()
        .route("/auth/profile", get(handlers::uesr::validateUser))
        .route_layer(middleware::from_fn(auth));

    // 跨域中间件
    let cors = CorsLayer::new().allow_origin(Any);

    let app = Router::new()
        // 合并router
        .merge(userRouter)
        .merge(postRouter)
        .merge(userProfileRouter)
        // 状态, 全路由可用的数据, 这里是数据连接池
        .with_state(pool)
        // 跨域
        .layer(cors)
        // 代码压缩层
        .layer(CompressionLayer::new())
        // http跟踪器
        .layer(TraceLayer::new_for_http());

    let listener = tokio::net::TcpListener::bind("127.0.0.1:4000")
        .await
        .unwrap();

    println!("listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app).await.unwrap();
}

鉴权中间件

/// main函数

#[derive(Clone)]
struct AuthHeader {
    id: String,
}

async fn auth(
    headers: HeaderMap,
    mut req: Request,
    next: Next,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    // 提取 Authorization header
    let header = headers.get("Authorization").ok_or((
        StatusCode::UNAUTHORIZED,
        "missing authorization header".to_string(),
    ))?;

    let header_str = header
        .to_str()
        .map_err(|_| (StatusCode::BAD_REQUEST, "invalid authorization header"))
        .unwrap();

    let token = header_str.replace("Bearer ", "").trim().to_string();

    // 验证 token
    let claims = validate_token(token.as_str()).map_err(|e| {
        tracing::warn!("token validation failed: {:?}", e);
        (
            StatusCode::UNAUTHORIZED,
            "invalid or expired token".to_string(),
        )
    })?;

    // 将用户信息注入请求上下文
    req.extensions_mut().insert(AuthHeader { id: claims.sub });

    Ok(next.run(req).await)
}

使用APIfox进行测试

APIfox是一个接口测试工具
apifox
本案例的接口我已经共享了:
接口文档

目前较成熟的二开框架

未知

社群

你可以在这些平台联系我:


网站公告

今日签到

点亮在社区的每一天
去签到