Skip to content

Salvo(赛风)Rust Web框架 ​

Salvo 是国产开源、基于 Tokio + Hyper 的异步 Rust Web 框架,中文文档完善,主打低样板代码、简洁API、全功能内置,对标 Axum / Actix‑web,目前版本 0.x(尚未1.0),核心已经稳定可用。

核心设计理念:Handler 就是 Middleware(中间件),没有区分两种不同类型,写普通函数就可以写中间件,大幅降低学习成本。

Salvo核心概念 ​

1. Handler 处理器 ​

用 #[handler] 宏标记异步函数,就是接口处理器。 参数可以按需省略,不需要全部写 Request, Depot, Response;

  • Request:请求对象,读取参数、body、header
  • Depot:请求上下文存储,用于中间件传递数据(比如登录用户信息)
  • Response:响应对象,返回json、文本、状态码
rust
use salvo::prelude::*;

#[handler]
async fn hello(res: &mut Response) {
    res.render("Hello Salvo");
}

#[tokio::main]
async fn main() {
    let router = Router::new().get(hello);
    let acceptor = TcpListener::new("127.0.0.1:7878").bind().await;
    Server::new(acceptor).serve(router).await;
}

2. Router 树状路由(核心特色) ​

Salvo 路由是树状嵌套路由,支持无限层级嵌套;.hoop() 挂载中间件,可以作用于整个子路由分支,实现局部鉴权。

rust
let router = Router::new()
    // 公开接口
    .push(Router::with_path("public").get(public_handler))
    // 需要鉴权的接口组,hoop 只作用于此分支
    .push(
        Router::with_path("api")
            .hoop(auth_middleware) // 该路由下全部接口都会经过鉴权
            .push(Router::with_path("user").get(get_user).post(create_user))
            .push(Router::with_path("article/{id}").get(get_article))
    );
  • {id} 路径参数;
  • .push() 嵌套子路由;
  • .hoop(mid):挂载中间件,可以作用到任意子路由,粒度非常灵活。

3. 中间件 hoop ​

中间件本质就是一个 #[handler] 函数。

  • 可以写在根路由(全局生效),也可以写在子路由(局部生效);
  • 通过 FlowCtrl 控制流程:调用 FlowCtrl::Next() 继续往下执行,FlowCtrl::Break() 直接终止请求,返回响应。
rust
#[handler]
async fn log_mid(req: &mut Request, depot: &mut Depot, res: &mut Response) -> FlowCtrl {
    println!("请求路径: {}", req.uri().path());
    FlowCtrl::Next() // 继续执行后续handler
}

4. 类型安全参数提取 Extractible ​

通过 derive 宏 Extractible,自动从请求的路径参数、query、body、header提取数据,编译期校验,不用手动解析json/query字符串。

rust
#[derive(Debug, Deserialize, Extractible)]
#[salvo(extract(default_source(from = "body")))]
struct UserReq {
    #[salvo(extract(source(from = "param")))]
    id: u64, // 从url路径{id}取
    name: String, // 从body json取
}

#[handler]
async fn update_user(req: UserReq) -> Json<UserReq> {
    Json(req)
}

5. OpenAPI 自动文档(一等公民支持) ​

把 #[handler] 换成 #[endpoint],框架自动生成 OpenAPI3 规范,内置Swagger UI,几乎零配置,不用手动写schema描述接口。

rust
#[endpoint]
async fn hello() -> &'static str {
    "hello"
}

6. Depot 请求上下文 ​

Depot 是每个请求独立的存储,中间件可以把数据放入Depot,后续handler直接读取。 例如鉴权中间件把登录用户id存入depot,业务handler直接拿。

rust
// 中间件存数据
depot.insert("user_id", 100u64);

// handler读取
let uid = depot.get::<u64>("user_id").unwrap();

Salvo内置主要功能 ​

  1. HTTP/1、HTTP/2、HTTP/3(QUIC) 完整支持,内置ACME自动HTTPS证书申请(Let’s Encrypt),不用额外工具管理证书。
  2. WebSocket、SSE 长连接原生支持。
  3. 内置JSON、表单、multipart文件上传、静态文件服务。
  4. 内置CORS、JWT、Session、压缩等常用中间件。
  5. 模板渲染(Tera)。
  6. salvo‑cli 脚手架工具,一键生成项目模板,支持SQLx/SeaORM数据库模板,JWT、CORS预设。
  7. 错误处理:支持返回Result,自动处理错误响应。

快速启动 ​

bash
cargo new salvo-demo
cd salvo-demo
cargo add salvo tokio --features salvo/oapi,tokio/macros

Salvo vs Axum vs Actix‑web ​

特性SalvoAxumActix‑web
底层Tokio+HyperTokio+Tower自己的运行时
中间件Handler即中间件,hoop,嵌套路由局部生效Tower中间件,泛型复杂Wrap,需要实现Trait
OpenAPI内置#[endpoint]自动生成需要第三方库需要第三方库
HTTP3✅内置需要额外配置✅支持
中文文档⭐⭐⭐⭐⭐(官方中文)⭐⭐⭐⭐
路由嵌套树状嵌套,支持局部中间件嵌套路由,中间件全局/路由级嵌套路由
版本0.x(未1.0)稳定稳定
生态国内社区活跃,国内项目多国际主流,生态庞大老牌高性能,生态成熟

Salvo优缺点 ​

✅ 优点

  1. API简洁,样板代码少,学习曲线平缓,对Rust新手友好。
  2. 中文官方文档,国内开发者友好。
  3. 树状路由+局部中间件,大型项目组织结构清晰。
  4. OpenAPI文档原生,不用额外插件。
  5. HTTP3、ACME自动HTTPS内置,开箱即用。
  6. 性能属于Rust Web第一梯队,benchmark表现优秀。

❌ 缺点

  1. 还没有发布1.0版本,版本迭代可能会有breaking change。
  2. 国际社区规模不如Axum,第三方生态库数量少于Axum。
  3. 部分高级场景需要自己找第三方crates。

适合与不适合场景 ​

✅适合

  • 后端API服务、RESTful接口
  • 需要自动OpenAPI文档的业务服务
  • 中小型Rust后端,希望快速开发,不想写大量模板代码
  • 需要HTTP3、自动HTTPS证书的服务
  • 国内开发者,偏好中文文档

❌不适合

  • 追求绝对稳定、要求1.0正式版本的大型企业核心业务;
  • 重度依赖大量第三方生态库的项目(优先Axum)。

Salvo常见配套生态 ​

  • ORM:SeaORM、SQLx、Diesel
  • 认证:salvo内置JWT,也可使用其他库
  • 数据库:PostgreSQL / MySQL / SQLite / MongoDB
  • 部署:编译为单二进制文件,直接部署,或者docker。

Salvo 完整可运行示例 ​

包含:嵌套路由、鉴权中间件、JSON接口、OpenAPI文档、请求参数提取、静态文件,复制即可编译运行。

Cargo.toml ​

toml
[package]
name = "salvo-demo"
version = "0.1.0"
edition = "2024"

[dependencies]
salvo = { version = "0.76", features = ["oapi", "websocket", "multipart", "static-files", "jwt"] }
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

src/main.rs ​

rust
use salvo::prelude::*;
use serde::{Deserialize, Serialize};

// -------------------------- 数据模型 --------------------------
#[derive(Debug, Serialize, Deserialize, Extractible)]
#[salvo(extract(default_source(from = "body")))]
pub struct CreateUser {
    pub name: String,
    pub age: u8,
}

#[derive(Debug, Serialize)]
pub struct User {
    pub id: u64,
    pub name: String,
    pub age: u8,
}

// -------------------------- 中间件:简单鉴权 --------------------------
#[handler]
async fn auth_mid(req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) -> FlowCtrl {
    // 模拟校验token
    let token = req.headers().get("Authorization").and_then(|v| v.to_str().ok());
    match token {
        Some(t) if t == "Bearer demo-token-123" => {
            // 把用户信息存入Depot,后续handler读取
            depot.insert("current_user", User {
                id: 1,
                name: "admin".into(),
                age: 25,
            });
            FlowCtrl::Next()
        }
        _ => {
            res.status_code(StatusCode::UNAUTHORIZED);
            res.render(Json(serde_json::json!({"msg":"未授权"})));
            FlowCtrl::Break()
        }
    }
}

// -------------------------- 接口处理器 --------------------------
/// 公开接口,不需要鉴权
#[endpoint]
async fn hello() -> &'static str {
    "Hello Salvo!"
}

/// 获取用户详情,路径参数 {id}
#[endpoint]
async fn get_user(#[salvo(extract(source(from = "param")))] id: u64) -> Json<User> {
    Json(User {
        id,
        name: format!("user_{id}"),
        age: 20,
    })
}

/// 创建用户,接收JSON body
#[endpoint]
async fn create_user(body: CreateUser) -> Json<User> {
    Json(User {
        id: 100,
        name: body.name,
        age: body.age,
    })
}

/// 读取Depot中鉴权后的用户信息
#[endpoint]
async fn current_user(depot: &mut Depot) -> Json<User> {
    let user = depot.get::<User>("current_user").unwrap();
    Json(user.clone())
}

#[tokio::main]
async fn main() {
    // 路由树
    let router = Router::new()
        // 公开路由
        .push(Router::with_path("/").get(hello))
        // 受保护API组,全部经过 auth_mid 鉴权
        .push(
            Router::with_path("/api")
                .hoop(auth_mid)
                .push(Router::with_path("user/{id}").get(get_user))
                .push(Router::with_path("user").post(create_user))
                .push(Router::with_path("me").get(current_user))
        )
        // OpenAPI文档路由
        .push(OpenApi::new("salvo-demo-api", ApiDoc::new()).swagger_ui());

    let acceptor = TcpListener::new("127.0.0.1:7878").bind().await;
    println!("服务启动:http://127.0.0.1:7878");
    println!("Swagger文档:http://127.0.0.1:7878/swagger-ui");
    Server::new(acceptor).serve(router).await;
}

运行 ​

bash
cargo run

访问地址 ​

  1. 主页:http://127.0.0.1:7878
  2. Swagger OpenAPI文档:http://127.0.0.1:7878/swagger-ui

测试受保护接口需要在请求头带上:Authorization: Bearer demo-token-123

关键知识点小结 ​

  1. #[endpoint]:自动生成OpenAPI文档;普通接口用#[handler]即可。
  2. hoop():中间件,可以挂载到任意子路由,只作用于该分支,粒度非常灵活。
  3. Depot:每个请求独立存储,中间件与handler之间传递数据。
  4. Extractible:自动提取路径参数、query、json body,编译期校验。
  5. FlowCtrl:Next()继续执行;Break()直接终止请求返回响应。

测试接口示例(curl) ​

bash
# 公开接口
curl http://127.0.0.1:7878

# 受保护接口,带上token
curl -H "Authorization: Bearer demo-token-123" http://127.0.0.1:7878/api/me

# 创建用户 POST json
curl -X POST -H "Authorization: Bearer demo-token-123" -H "Content-Type: application/json" -d '{"name":"test","age":22}' http://127.0.0.1:7878/api/user

Salvo 扩展示例 ​

Salvo 扩展示例:SeaORM + WebSocket + 静态文件 + JWT 基于上面的项目继续扩展,包含:

  1. SeaORM ORM 数据库操作(SQLite)
  2. WebSocket 长连接示例
  3. 静态文件服务
  4. 真实 JWT 签发&校验鉴权

Cargo.toml ​

toml
[package]
name = "salvo-demo"
version = "0.1.0"
edition = "2024"

[dependencies]
salvo = { version = "0.76", features = ["oapi", "websocket", "multipart", "static-files", "jwt"] }
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sea-orm = { version = "1.0", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros"] }
sea-orm-migration = "1.0"
jsonwebtoken = "9.0"
chrono = { version = "0.4", features = ["serde"] }

src/main.rs ​

rust
use chrono::{Duration, Utc};
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use salvo::prelude::*;
use sea_orm::{Database, DatabaseConnection, EntityTrait, IntoActiveModel, Set};
use serde::{Deserialize, Serialize};

// -------------------------- SeaORM 实体定义 --------------------------
mod entity {
    use sea_orm::entity::prelude::*;

    #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize)]
    #[sea_orm(table_name = "users")]
    pub struct Model {
        #[sea_orm(primary_key)]
        pub id: i64,
        pub name: String,
        pub age: i32,
    }

    #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
    pub enum Relation {}

    impl ActiveModelBehavior for ActiveModel {}
}

use entity::{Entity as UserEntity, Model as UserModel};

// -------------------------- JWT 配置 --------------------------
const JWT_SECRET: &[u8] = b"my-secret-key-123456";

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

// 生成JWT Token
fn generate_jwt(user_id: i64) -> String {
    let expiration = Utc::now() + Duration::hours(24);
    let claims = Claims {
        sub: user_id,
        exp: expiration.timestamp(),
    };
    let header = Header::new(Algorithm::HS256);
    encode(&header, &claims, &EncodingKey::from_secret(JWT_SECRET)).unwrap()
}

// -------------------------- 请求模型 --------------------------
#[derive(Debug, Deserialize, Extractible)]
#[salvo(extract(default_source(from = "body")))]
pub struct LoginReq {
    pub name: String,
}

#[derive(Debug, Serialize)]
pub struct LoginResp {
    pub token: String,
}

// -------------------------- JWT鉴权中间件 --------------------------
#[handler]
async fn jwt_auth(depot: &mut Depot, req: &mut Request, res: &mut Response, ctrl: &mut FlowCtrl) -> FlowCtrl {
    let auth_header = match req.headers().get("Authorization") {
        Some(h) => h.to_str().ok(),
        None => None,
    };
    let token_str = match auth_header {
        Some(s) if s.starts_with("Bearer ") => &s[7..],
        _ => {
            res.status_code(StatusCode::UNAUTHORIZED);
            res.render(Json(serde_json::json!({"msg":"需要Bearer token"})));
            return FlowCtrl::Break();
        }
    };

    // 校验jwt
    let token_data = match jsonwebtoken::decode::<Claims>(
        token_str,
        &jsonwebtoken::DecodingKey::from_secret(JWT_SECRET),
        &jsonwebtoken::Validation::new(Algorithm::HS256),
    ) {
        Ok(d) => d,
        Err(_) => {
            res.status_code(StatusCode::UNAUTHORIZED);
            res.render(Json(serde_json::json!({"msg":"token无效"})));
            return FlowCtrl::Break();
        }
    };
    depot.insert("user_id", token_data.claims.sub);
    FlowCtrl::Next()
}

// -------------------------- WebSocket处理器 --------------------------
#[handler]
async fn ws_echo(req: &mut Request, res: &mut Response) {
    let ws = match WebSocket::upgrade(req, res).await {
        Ok(ws) => ws,
        Err(_) => return,
    };
    let (mut tx, mut rx) = ws.split();
    while let Some(msg) = rx.recv().await {
        if let Ok(msg) = msg {
            if msg.is_text() || msg.is_binary() {
                tx.send(msg).await.ok();
            }
        } else {
            break;
        }
    }
}

// -------------------------- API接口 --------------------------
/// 登录接口,返回JWT token
#[endpoint]
async fn login(body: LoginReq, db: &State<DatabaseConnection>) -> Json<LoginResp> {
    // 模拟查询用户
    let user = UserEntity::find()
        .filter(entity::Column::Name.eq(body.name))
        .one(db)
        .await
        .unwrap();

    let user = match user {
        Some(u) => u,
        None => {
            // 不存在则新建用户
            let new_user = entity::ActiveModel {
                name: Set(body.name),
                age: Set(18),
                ..Default::default()
            };
            UserEntity::insert(new_user).exec(db).await.unwrap().last_insert_id
        }
    };
    let token = generate_jwt(user.id);
    Json(LoginResp { token })
}

/// 获取当前登录用户信息(需要jwt鉴权)
#[endpoint]
async fn get_me(depot: &mut Depot, db: &State<DatabaseConnection>) -> Json<UserModel> {
    let uid = depot.get::<i64>("user_id").unwrap();
    let user = UserEntity::find_by_id(*uid).one(db).await.unwrap().unwrap();
    Json(user)
}

#[tokio::main]
async fn main() {
    // 1. 初始化数据库 SQLite
    let db = Database::connect("sqlite://./demo.db?mode=rwc").await.unwrap();
    // 执行迁移(简单示例,实际项目使用 sea‑orm‑migration)
    sea_orm_migration::create_table!(
        db,
        users,
        id integer primary key autoincrement,
        name text not null,
        age integer not null
    )
    .await
    .unwrap();

    // 2. 路由
    let router = Router::new()
        // 静态文件:访问 /static/* 读取 ./static 目录
        .push(StaticFiles::new("./static").with_path("static"))
        // WebSocket
        .push(Router::with_path("ws").get(ws_echo))
        // 公开接口:登录
        .push(Router::with_path("login").post(login))
        // 需要JWT鉴权的API分组
        .push(
            Router::with_path("api")
                .hoop(jwt_auth)
                .push(Router::with_path("me").get(get_me))
        )
        // OpenAPI文档
        .push(OpenApi::new("salvo‑demo‑full", ApiDoc::new()).swagger_ui());

    // 注入数据库连接到State全局状态
    let router = router.with_state(db);

    let acceptor = TcpListener::new("127.0.0.1:7878").bind().await;
    println!("服务启动: http://127.0.0.1:7878");
    println!("Swagger: http://127.0.0.1:7878/swagger-ui");
    println!("WebSocket: ws://127.0.0.1:7878/ws");
    Server::new(acceptor).serve(router).await;
}

运行步骤 ​

  1. 创建静态资源文件夹 static,里面放任意html/css文件,访问 http://127.0.0.1:7878/static/xxx.html
  2. 执行 cargo run,会自动生成 demo.db sqlite数据库。

测试curl示例 ​

bash
# 登录,获取token
curl -X POST -H "Content-Type: application/json" -d '{"name":"alice"}' http://127.0.0.1:7878/login

# 使用token访问受保护接口
curl -H "Authorization: Bearer 这里替换成返回的token" http://127.0.0.1:7878/api/me

WebSocket测试 ​

可以使用在线ws测试工具连接:ws://127.0.0.1:7878/ws,发送消息会原样回显。

重点知识点 ​

  1. State<T>:全局共享状态,把数据库连接注入路由,所有handler都可以获取。
  2. JWT鉴权中间件:hoop(jwt_auth) 挂载到 /api 分支,该分支全部接口强制校验token。
  3. WebSocket:WebSocket::upgrade 升级http连接,收发消息。
  4. StaticFiles:内置静态文件服务,可托管前端静态资源。
  5. SeaORM:ORM操作SQLite,简单建表,CRUD。

生产环境注意事项 ​

  1. JWT密钥不要硬编码,使用环境变量读取。
  2. 数据库迁移建议使用 sea‑orm‑migration 独立迁移文件,不要在main函数直接建表。
  3. 生产建议开启HTTPS,Salvo内置ACME自动证书。
  4. 错误处理:示例为简单演示,真实业务需要完善错误捕获。