Rust 学习之 Axum Web 应用程序框架
·
1min
·
Paxon Qiao
Table of Contents
Rust 学习之 Axum Web 应用程序框架
Axum 是一个关注人机工程学和模块化的 Web 应用程序框架。
https://docs.rs/axum/latest/axum/
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<&'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,
},
])
}
// "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6IlhpYW8gUWlhbyJ9.7PdpzWjZLN4KKNLoM07nfnhKnYdrc0IjumKcOUREXzI"
async fn create_todo_handler(claims: Claims, Json(todo): Json<CreateTodo>) -> StatusCode {
println!("{:?}", claims);
println!("{:?}", todo);
StatusCode::CREATED
}
async fn login_handler(Json(login): Json<LoginRequest>) -> Json<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<S, B> FromRequest<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<B>, state: &S) -> Result<Self, Self::Rejection> {
// // // ...
// // let TypedHeader(Authorization(bearer)) =
// // TypedHeader::<Authorization<Bearer>>::from_request(req, state)
// // .await
// // .map_err(|_| http::StatusCode::NETWORK_AUTHENTICATION_REQUIRED)?;
// // let key = jwt::DecodingKey::from_secret(SECRET);
// // let claims = jwt::decode::<Claims>(bearer.token(), &key, &jwt::Validation::default())
// // .map_err(|_| http::StatusCode::UNAUTHORIZED)?;
// // Ok(claims.claims)
// // }
// // }
// {
// type Rejection = HttpError;
// async fn from_request(req: Request<B>, state: &S) -> Result<Self, Self::Rejection> {
// // ...
// let TypedHeader(Authorization(bearer)) =
// TypedHeader::<Authorization<Bearer>>::from_request(req, state)
// .await
// .map_err(|_| HttpError::Auth)?;
// let key = jwt::DecodingKey::from_secret(SECRET);
// let token = jwt::decode::<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<S> FromRequestParts<S> for Claims
where
S: Send + Sync,
{
type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
// Extract the token from the authorization header
let TypedHeader(Authorization(bearer)) = parts
.extract::<TypedHeader<Authorization<Bearer>>>()
.await
.map_err(|_| AuthError::InvalidToken)?;
let key = jwt::DecodingKey::from_secret(SECRET);
// Decode the user data
let token_data = jwt::decode::<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