从零开始:用 Rust 和 Axum 打造高效 Web 应用

从零开始:用Rust和Axum打造高效Web应用Rust以其高性能和安全性席卷开发圈,而Axum作为一款专注于人机工程学和模块化的Web框架,正成为Rust生态中的明星工具。想快速上手一个现代Web应用框本文详细介绍了如何使用Rust的Axum框架搭建We

从零开始:用 Rust 和 Axum 打造高效 Web 应用

Rust 以其高性能和安全性席卷开发圈,而 Axum 作为一款专注于人机工程学和模块化的 Web 框架,正成为 Rust 生态中的明星工具。想快速上手一个现代 Web 应用框

本文详细介绍了如何使用 Rust 的 Axum 框架搭建 Web 应用程序。从项目初始化、依赖配置,到实现基本的路由、API 端点和 JWT 认证功能,每一步都配有清晰的代码示例和运行说明。无论是返回简单的 “Hello, World!”,还是处理 JSON 数据及用户登录验证,你都能在这里找到实用指南。文章还结合了 jsonwebtoken 的集成,展示了如何为你的 Web 服务添加安全认证能力,适合 Rust 初学者和进阶开发者参考。

Axum 实操

创建项目并用VSCode 打开

~ via 🅒 base
➜ cd Code/rust

~/Code/rust via 🅒 base
➜ cargo new --lib axum-live
     Created library `axum-live` package

~/Code/rust via 🅒 base
➜ cd axum-live

axum-live on  master [?] via 🦀 1.72.0 via 🅒 base
➜ c

添加相关依赖并创建示例文件

axum-live on  master [?] via 🦀 1.72.0 via 🅒 base 
➜ cargo add axum           

axum-live on  master [?] is 📦 0.1.0 via 🦀 1.72.0 via 🅒 base took 2.6s 
➜ cargo add anyhow

axum-live on  master [?] is 📦 0.1.0 via 🦀 1.72.0 via 🅒 base 
➜ cargo add tokio --features full

axum-live on  master [?] is 📦 0.1.0 via 🦀 1.72.0 via 🅒 base 
➜ mkdir examples

axum-live on  master [?] is 📦 0.1.0 via 🦀 1.72.0 via 🅒 base 
➜ touch examples/basic.rs    

axum-live on  master [?] is 📦 0.1.0 via 🦀 1.72.0 via 🅒 base 
➜ cargo add serde --features derive
axum-live on  master [?] is 📦 0.1.0 via 🦀 1.72.0 via 🅒 base took 3.8s 
➜ cargo add jsonwebtoken      

examples/basic.rs

use std::net::SocketAddr;

use axum::{http::StatusCode, response::Html, routing::get, Json, Router, Server};
use serde::{Deserialize, Serialize};

// 定义一个 Todo 结构体
#[derive(Serialize, Deserialize, Debug)]
pub struct Todo {
    pub id: usize,
    pub title: String,
    pub completed: bool,
}

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

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(index_handler))
        .route("/todos", get(todos_handler).post(create_todo_handler));

    let addr = SocketAddr::from(([127, 0, 0, 1], 8000));
    println!("listening on http://{}", addr);

    Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn index_handler() -> Html<&'static str> {
    Html("<h1>Hello, World!</h1>")
}

async fn todos_handler() -> Json<Vec<Todo>> {
    Json(vec![
        Todo {
            id: 1,
            title: "Buy milk".to_string(),
            completed: false,
        },
        Todo {
            id: 2,
            title: "Buy eggs".to_string(),
            completed: false,
        },
    ])
}

async fn create_todo_handler(Json(todo): Json<CreateTodo>) -> StatusCode {
    println!("{:?}", todo);
    StatusCode::CREATED
}

Cargo.toml

[package]
name = "axum-live"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1.0.75"
axum = "0.6.20"
jsonwebtoken = "8.3.0"
serde = { version = "1.0.188", features = ["derive"] }
tokio = { version = "1.32.0", features = ["full"] }

运行

axum-live on  master [?] is 📦 0.1.0 via 🦀 1.72.0 via 🅒 base 
➜ cargo run --example basic        
listening on http://127.0.0.1:8000

basic.http

http://localhost:8000/

### 
// todos_handler
GET http://localhost:8000/todos

###
POST http://localhost:8000/todos HTTP/1.1
content-type: application/json

{
    "title": "larry"
}

jsonwebtoken

<https://docs.rs/jsonwebtoken/latest/jsonwebtoken/>

<https://github.com/tokio-rs/axum/blob/main/examples/jwt/src/main.rs>

examples/basic.rs

use std::{net::SocketAddr, time::SystemTime};

use axum::{
    async_trait,
    extract::{FromRequest, FromRequestParts},
    headers::{authorization::Bearer, Authorization},
    http::{self, Request},
    http::{request::Parts, StatusCode},
    response::{Html, IntoResponse, Response},
    routing::{get, post},
    Json, RequestPartsExt, Router, Server, TypedHeader,
};
use jsonwebtoken as jwt;
use jwt::Validation;
use serde::{Deserialize, Serialize};
use serde_json::json;

const SECRET: &[u8] = b"deadbeef";

// 定义一个 Todo 结构体
#[derive(Serialize, Deserialize, Debug)]
pub struct Todo {
    pub id: usize,
    pub title: String,
    pub completed: bool,
}

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

#[derive(Serialize, Deserialize, Debug)]
struct LoginRequest {
    email: String,
    password: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct LoginResponse {
    token: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    id: usize,
    name: String,
    exp: usize,
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(index_handler))
        .route("/todos", get(todos_handler).post(create_todo_handler))
        .route("/login", post(login_handler));

    let addr = SocketAddr::from(([127, 0, 0, 1], 8000));
    println!("listening on http://{}", addr);

    Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn index_handler() -> Html&lt;&'static str> {
    Html("&lt;h1>Hello, World!&lt;/h1>")
}

async fn todos_handler() -> Json&lt;Vec&lt;Todo>> {
    Json(vec![
        Todo {
            id: 1,
            title: "Buy milk".to_string(),
            completed: false,
        },
        Todo {
            id: 2,
            title: "Buy eggs".to_string(),
            completed: false,
        },
    ])
}

// "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6IlhpYW8gUWlhbyJ9.7PdpzWjZLN4KKNLoM07nfnhKnYdrc0IjumKcOUREXzI"
async fn create_todo_handler(claims: Claims, Json(todo): Json&lt;CreateTodo>) -> StatusCode {
    println!("{:?}", claims);
    println!("{:?}", todo);
    StatusCode::CREATED
}

async fn login_handler(Json(login): Json&lt;LoginRequest>) -> Json&lt;LoginResponse> {
    // skip login info validation
    println!("{:?}", login);

    let claims = Claims {
        id: 1,
        name: "Xiao Qiao".to_string(),
        exp: get_epoch() + 14 * 24 * 60 * 60,
    };
    let key = jwt::EncodingKey::from_secret(SECRET);
    let token = jwt::encode(&jwt::Header::default(), &claims, &key).unwrap();
    Json(LoginResponse { token })
}

// #[async_trait]
// impl&lt;S, B> FromRequest&lt;S, B> for Claims
// where
//     // these bounds are required by `async_trait`
//     B: Send + 'static,
//     S: Send + Sync,
//     // {
//     //     type Rejection = http::StatusCode;

//     //     async fn from_request(req: Request&lt;B>, state: &S) -> Result&lt;Self, Self::Rejection> {
//     //         // ...
//     //         let TypedHeader(Authorization(bearer)) =
//     //             TypedHeader::&lt;Authorization&lt;Bearer>>::from_request(req, state)
//     //                 .await
//     //                 .map_err(|_| http::StatusCode::NETWORK_AUTHENTICATION_REQUIRED)?;

//     //         let key = jwt::DecodingKey::from_secret(SECRET);
//     //         let claims = jwt::decode::&lt;Claims>(bearer.token(), &key, &jwt::Validation::default())
//     //             .map_err(|_| http::StatusCode::UNAUTHORIZED)?;
//     //         Ok(claims.claims)
//     //     }
//     // }
// {
//     type Rejection = HttpError;

//     async fn from_request(req: Request&lt;B>, state: &S) -> Result&lt;Self, Self::Rejection> {
//         // ...
//         let TypedHeader(Authorization(bearer)) =
//             TypedHeader::&lt;Authorization&lt;Bearer>>::from_request(req, state)
//                 .await
//                 .map_err(|_| HttpError::Auth)?;

//         let key = jwt::DecodingKey::from_secret(SECRET);
//         let token = jwt::decode::&lt;Claims>(bearer.token(), &key, &Validation::default())
//             .map_err(|_| HttpError::Auth)?;
//         Ok(token.claims)
//     }
// }

// #[derive(Debug)]
// enum HttpError {
//     Auth,
//     Internal,
//     NotFound,
//     InternalServerError,
// }

// impl IntoResponse for HttpError {
//     fn into_response(self) -> axum::response::Response {
//         let (code, msg) = match self {
//             HttpError::Auth => (StatusCode::UNAUTHORIZED, "Unauthorized"),
//             HttpError::NotFound => (StatusCode::NOT_FOUND, "Not Found"),
//             HttpError::InternalServerError => {
//                 (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error")
//             }
//             HttpError::Internal => (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error"),
//         };
//         (code, msg).into_response()
//     }
// }

fn get_epoch() -> usize {
    SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)
        .unwrap()
        .as_secs() as usize
}

#[async_trait]
impl&lt;S> FromRequestParts&lt;S> for Claims
where
    S: Send + Sync,
{
    type Rejection = AuthError;

    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result&lt;Self, Self::Rejection> {
        // Extract the token from the authorization header
        let TypedHeader(Authorization(bearer)) = parts
            .extract::&lt;TypedHeader&lt;Authorization&lt;Bearer>>>()
            .await
            .map_err(|_| AuthError::InvalidToken)?;
        let key = jwt::DecodingKey::from_secret(SECRET);
        // Decode the user data
        let token_data = jwt::decode::&lt;Claims>(bearer.token(), &key, &Validation::default())
            .map_err(|_| AuthError::InvalidToken)?;

        Ok(token_data.claims)
    }
}

impl IntoResponse for AuthError {
    fn into_response(self) -> Response {
        let (status, error_message) = match self {
            // AuthError::WrongCredentials => (StatusCode::UNAUTHORIZED, "Wrong credentials"),
            // AuthError::MissingCredentials => (StatusCode::BAD_REQUEST, "Missing credentials"),
            // AuthError::TokenCreation => (StatusCode::INTERNAL_SERVER_ERROR, "Token creation error"),
            AuthError::InvalidToken => (StatusCode::BAD_REQUEST, "Invalid token"),
        };
        let body = Json(json!({
            "error": error_message,
        }));
        (status, body).into_response()
    }
}

#[derive(Debug)]
enum AuthError {
    // WrongCredentials,
    // MissingCredentials,
    // TokenCreation,
    InvalidToken,
}

basic.http

GET http://localhost:8000/ HTTP/1.1

### 
// todos_handler
GET http://localhost:8000/todos HTTP/1.1

###
POST http://localhost:8000/todos HTTP/1.1
content-type: application/json

{
    "title": "larry"
}

###
POST http://localhost:8000/login HTTP/1.1
content-type: application/json

{
    "email": "xiaoqiao@gmail.com",
    "password": "123456"
}

###
POST http://localhost:8000/todos HTTP/1.1
content-type: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6IlhpYW8gUWlhbyIsImV4cCI6MTY5NDk0NDQzNH0.FhUtNuyUtCA-gtMckc3UkvTu3Z2Ek8DYH1lg8PNXDmk

{
    "title": "hello world"
}

响应

HTTP/1.1 201 Created
content-length: 0
date: Sun, 03 Sep 2023 15:58:29 GMT

总结

通过本文的实战演练,我们从创建 Rust 项目开始,一步步探索了 Axum 框架的强大功能:简单的路由设计、灵活的请求处理,以及与 jsonwebtoken 结合实现的认证机制。Axum 不仅继承了 Rust 的性能优势,还以模块化和易用性降低了 Web 开发的门槛。无论你是 Rust 新手还是想提升 Web 开发技能的开发者,Axum 都值得一试。现在就动手实践,开启你的 Rust Web 开发之旅吧!

参考

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
寻月隐君
寻月隐君
0x89EE...a439
不要放弃,如果你喜欢这件事,就不要放弃。如果你不喜欢,那这也不好,因为一个人不应该做自己不喜欢的事。