salvo-auth

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Salvo Authentication

Salvo 认证

This skill helps implement authentication and authorization in Salvo applications.
本技能可帮助你在Salvo应用中实现认证与授权功能。

JWT Authentication

JWT 认证

Setup

配置

toml
[dependencies]
salvo = { version = "0.89.0", features = ["jwt-auth"] }
jsonwebtoken = "9"
serde = { version = "1", features = ["derive"] }
chrono = "0.4"
toml
[dependencies]
salvo = { version = "0.89.0", features = ["jwt-auth"] }
jsonwebtoken = "9"
serde = { version = "1", features = ["derive"] }
chrono = "0.4"

JWT Middleware Setup

JWT 中间件配置

rust
use salvo::jwt_auth::{JwtAuth, JwtClaims};
use serde::{Deserialize, Serialize};

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

const SECRET_KEY: &str = "your-secret-key-at-least-32-bytes";

#[tokio::main]
async fn main() {
    let auth_handler = JwtAuth::new("secret_key")
        .finders(vec![
            Box::new(HeaderFinder::new()),
            Box::new(QueryFinder::new("token")),
        ]);

    let router = Router::new()
        .push(Router::with_path("login").post(login))
        .push(
            Router::with_path("protected")
                .hoop(auth_handler)
                .get(protected_handler)
        );

    let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
    Server::new(acceptor).serve(router).await;
}
rust
use salvo::jwt_auth::{JwtAuth, JwtClaims};
use serde::{Deserialize, Serialize};

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

const SECRET_KEY: &str = "your-secret-key-at-least-32-bytes";

#[tokio::main]
async fn main() {
    let auth_handler = JwtAuth::new("secret_key")
        .finders(vec![
            Box::new(HeaderFinder::new()),
            Box::new(QueryFinder::new("token")),
        ]);

    let router = Router::new()
        .push(Router::with_path("login").post(login))
        .push(
            Router::with_path("protected")
                .hoop(auth_handler)
                .get(protected_handler)
        );

    let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
    Server::new(acceptor).serve(router).await;
}

Login Handler

登录处理器

rust
use jsonwebtoken::{encode, EncodingKey, Header};
use salvo::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct LoginRequest {
    username: String,
    password: String,
}

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

#[handler]
async fn login(body: JsonBody<LoginRequest>) -> Result<Json<LoginResponse>, StatusError> {
    let req = body.into_inner();

    // Validate credentials (replace with actual validation)
    if req.username != "admin" || req.password != "password" {
        return Err(StatusError::unauthorized());
    }

    // Create JWT claims
    let claims = JwtClaims {
        sub: req.username,
        exp: (chrono::Utc::now() + chrono::Duration::hours(24)).timestamp(),
        role: "user".to_string(),
    };

    // Encode JWT
    let token = encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(SECRET_KEY.as_bytes()),
    )
    .map_err(|_| StatusError::internal_server_error())?;

    Ok(Json(LoginResponse { token }))
}
rust
use jsonwebtoken::{encode, EncodingKey, Header};
use salvo::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct LoginRequest {
    username: String,
    password: String,
}

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

#[handler]
async fn login(body: JsonBody<LoginRequest>) -> Result<Json<LoginResponse>, StatusError> {
    let req = body.into_inner();

    // 验证凭据(替换为实际验证逻辑)
    if req.username != "admin" || req.password != "password" {
        return Err(StatusError::unauthorized());
    }

    // 创建JWT声明
    let claims = JwtClaims {
        sub: req.username,
        exp: (chrono::Utc::now() + chrono::Duration::hours(24)).timestamp(),
        role: "user".to_string(),
    };

    // 编码JWT
    let token = encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(SECRET_KEY.as_bytes()),
    )
    .map_err(|_| StatusError::internal_server_error())?;

    Ok(Json(LoginResponse { token }))
}

Protected Handler

受保护接口处理器

rust
use salvo::jwt_auth::JwtAuthDepotExt;

#[handler]
async fn protected_handler(depot: &mut Depot) -> Result<String, StatusError> {
    let token_data = depot.jwt_auth_data::<JwtClaims>()
        .ok_or_else(|| StatusError::unauthorized())?;

    Ok(format!("Hello, {}! Role: {}", token_data.claims.sub, token_data.claims.role))
}
rust
use salvo::jwt_auth::JwtAuthDepotExt;

#[handler]
async fn protected_handler(depot: &mut Depot) -> Result<String, StatusError> {
    let token_data = depot.jwt_auth_data::<JwtClaims>()
        .ok_or_else(|| StatusError::unauthorized())?;

    Ok(format!("Hello, {}! Role: {}", token_data.claims.sub, token_data.claims.role))
}

Basic Authentication

Basic 认证

Setup

配置

toml
[dependencies]
salvo = { version = "0.89.0", features = ["basic-auth"] }
toml
[dependencies]
salvo = { version = "0.89.0", features = ["basic-auth"] }

Basic Auth Middleware

Basic Auth 中间件

rust
use salvo::prelude::*;
use salvo::basic_auth::{BasicAuth, BasicAuthValidator};

struct MyValidator;

impl BasicAuthValidator for MyValidator {
    async fn validate(&self, username: &str, password: &str, depot: &mut Depot) -> bool {
        if username == "admin" && password == "password" {
            depot.insert("user_role", "admin");
            true
        } else if username == "user" && password == "userpass" {
            depot.insert("user_role", "user");
            true
        } else {
            false
        }
    }
}

#[tokio::main]
async fn main() {
    let auth_handler = BasicAuth::new(MyValidator);

    let router = Router::new()
        .push(
            Router::with_path("admin")
                .hoop(auth_handler)
                .get(admin_handler)
        );

    let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
    Server::new(acceptor).serve(router).await;
}
rust
use salvo::prelude::*;
use salvo::basic_auth::{BasicAuth, BasicAuthValidator};

struct MyValidator;

impl BasicAuthValidator for MyValidator {
    async fn validate(&self, username: &str, password: &str, depot: &mut Depot) -> bool {
        if username == "admin" && password == "password" {
            depot.insert("user_role", "admin");
            true
        } else if username == "user" && password == "userpass" {
            depot.insert("user_role", "user");
            true
        } else {
            false
        }
    }
}

#[tokio::main]
async fn main() {
    let auth_handler = BasicAuth::new(MyValidator);

    let router = Router::new()
        .push(
            Router::with_path("admin")
                .hoop(auth_handler)
                .get(admin_handler)
        );

    let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
    Server::new(acceptor).serve(router).await;
}

Custom Authentication Middleware

自定义认证中间件

rust
use salvo::prelude::*;

#[handler]
async fn auth_middleware(req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
    let token = req.header::<String>("Authorization")
        .and_then(|h| h.strip_prefix("Bearer ").map(String::from));

    match token {
        Some(token) => {
            match validate_token(&token) {
                Ok(user_id) => {
                    depot.insert("user_id", user_id);
                    ctrl.call_next(req, depot, res).await;
                }
                Err(_) => {
                    res.status_code(StatusCode::UNAUTHORIZED);
                    res.render(Json(serde_json::json!({"error": "Invalid token"})));
                    ctrl.skip_rest();
                }
            }
        }
        None => {
            res.status_code(StatusCode::UNAUTHORIZED);
            res.render(Json(serde_json::json!({"error": "Missing token"})));
            ctrl.skip_rest();
        }
    }
}

fn validate_token(token: &str) -> Result<i64, ()> {
    // Implement token validation logic
    if token == "valid_token" {
        Ok(123)
    } else {
        Err(())
    }
}
rust
use salvo::prelude::*;

#[handler]
async fn auth_middleware(req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
    let token = req.header::<String>("Authorization")
        .and_then(|h| h.strip_prefix("Bearer ").map(String::from));

    match token {
        Some(token) => {
            match validate_token(&token) {
                Ok(user_id) => {
                    depot.insert("user_id", user_id);
                    ctrl.call_next(req, depot, res).await;
                }
                Err(_) => {
                    res.status_code(StatusCode::UNAUTHORIZED);
                    res.render(Json(serde_json::json!({"error": "无效令牌"})));
                    ctrl.skip_rest();
                }
            }
        }
        None => {
            res.status_code(StatusCode::UNAUTHORIZED);
            res.render(Json(serde_json::json!({"error": "缺少令牌"})));
            ctrl.skip_rest();
        }
    }
}

fn validate_token(token: &str) -> Result<i64, ()> {
    // 实现令牌验证逻辑
    if token == "valid_token" {
        Ok(123)
    } else {
        Err(())
    }
}

API Key Authentication

API Key 认证

rust
#[handler]
async fn api_key_auth(req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
    let api_key = req.header::<String>("X-API-Key");

    match api_key {
        Some(key) if is_valid_api_key(&key) => {
            depot.insert("api_key", key);
            ctrl.call_next(req, depot, res).await;
        }
        _ => {
            res.status_code(StatusCode::UNAUTHORIZED);
            res.render("Invalid API key");
            ctrl.skip_rest();
        }
    }
}

fn is_valid_api_key(key: &str) -> bool {
    // Validate against database or config
    key == "valid-api-key-12345"
}
rust
#[handler]
async fn api_key_auth(req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
    let api_key = req.header::<String>("X-API-Key");

    match api_key {
        Some(key) if is_valid_api_key(&key) => {
            depot.insert("api_key", key);
            ctrl.call_next(req, depot, res).await;
        }
        _ => {
            res.status_code(StatusCode::UNAUTHORIZED);
            res.render("无效API密钥");
            ctrl.skip_rest();
        }
    }
}

fn is_valid_api_key(key: &str) -> bool {
    // 与数据库或配置进行验证
    key == "valid-api-key-12345"
}

Session-Based Authentication

基于会话的认证

rust
use salvo::prelude::*;
use salvo::session::{SessionHandler, CookieStore, SessionDepotExt};

#[tokio::main]
async fn main() {
    let session_handler = SessionHandler::builder(
        CookieStore::new(),
        b"secret_key_must_be_at_least_64_bytes_long_for_security_reasons!!",
    )
    .build()
    .unwrap();

    let router = Router::new()
        .hoop(session_handler)
        .push(Router::with_path("login").post(login))
        .push(Router::with_path("profile").get(profile));

    let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
    Server::new(acceptor).serve(router).await;
}

#[handler]
async fn login(depot: &mut Depot) -> StatusCode {
    let session = depot.session_mut().unwrap();
    session.insert("user_id", 123).unwrap();
    StatusCode::OK
}

#[handler]
async fn profile(depot: &mut Depot) -> Result<String, StatusError> {
    let session = depot.session().unwrap();
    let user_id: Option<i64> = session.get("user_id");

    match user_id {
        Some(id) => Ok(format!("User ID: {}", id)),
        None => Err(StatusError::unauthorized()),
    }
}
rust
use salvo::prelude::*;
use salvo::session::{SessionHandler, CookieStore, SessionDepotExt};

#[tokio::main]
async fn main() {
    let session_handler = SessionHandler::builder(
        CookieStore::new(),
        b"secret_key_must_be_at_least_64_bytes_long_for_security_reasons!!",
    )
    .build()
    .unwrap();

    let router = Router::new()
        .hoop(session_handler)
        .push(Router::with_path("login").post(login))
        .push(Router::with_path("profile").get(profile));

    let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
    Server::new(acceptor).serve(router).await;
}

#[handler]
async fn login(depot: &mut Depot) -> StatusCode {
    let session = depot.session_mut().unwrap();
    session.insert("user_id", 123).unwrap();
    StatusCode::OK
}

#[handler]
async fn profile(depot: &mut Depot) -> Result<String, StatusError> {
    let session = depot.session().unwrap();
    let user_id: Option<i64> = session.get("user_id");

    match user_id {
        Some(id) => Ok(format!("用户ID: {}", id)),
        None => Err(StatusError::unauthorized()),
    }
}

Role-Based Access Control (RBAC)

基于角色的访问控制(RBAC)

rust
#[derive(Clone)]
enum Role {
    Admin,
    User,
    Guest,
}

#[handler]
fn require_role(required: Role) -> impl Handler {
    move |depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl| async move {
        let user_role = depot.get::<Role>("user_role");

        match user_role {
            Some(role) if matches!((role, &required), (Role::Admin, _) | (Role::User, Role::User)) => {
                ctrl.call_next(req, depot, res).await;
            }
            _ => {
                res.status_code(StatusCode::FORBIDDEN);
                res.render("Insufficient permissions");
                ctrl.skip_rest();
                return;
            }
        }
    }
    check_role
}

let router = Router::new()
    .push(
        Router::with_path("admin")
            .hoop(auth_middleware)
            .hoop(RequireRole::new(Role::Admin))
            .get(admin_handler)
    )
    .push(
        Router::with_path("user")
            .hoop(auth_middleware)
            .hoop(require_role(Role::User))
            .get(user_handler)
    );
rust
#[derive(Clone)]
enum Role {
    Admin,
    User,
    Guest,
}

#[handler]
fn require_role(required: Role) -> impl Handler {
    move |depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl| async move {
        let user_role = depot.get::<Role>("user_role");

        match user_role {
            Some(role) if matches!((role, &required), (Role::Admin, _) | (Role::User, Role::User)) => {
                ctrl.call_next(req, depot, res).await;
            }
            _ => {
                res.status_code(StatusCode::FORBIDDEN);
                res.render("权限不足");
                ctrl.skip_rest();
                return;
            }
        }
    }
    check_role
}

let router = Router::new()
    .push(
        Router::with_path("admin")
            .hoop(auth_middleware)
            .hoop(RequireRole::new(Role::Admin))
            .get(admin_handler)
    )
    .push(
        Router::with_path("user")
            .hoop(auth_middleware)
            .hoop(require_role(Role::User))
            .get(user_handler)
    );

JWT with Refresh Tokens

带刷新令牌的JWT

rust
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct RefreshRequest {
    refresh_token: String,
}

#[derive(Serialize)]
struct TokenResponse {
    access_token: String,
    refresh_token: String,
    expires_in: i64,
}

#[handler]
async fn refresh_token(body: JsonBody<RefreshRequest>) -> Result<Json<TokenResponse>, StatusError> {
    let req = body.into_inner();

    // Validate refresh token (check database/cache)
    let user_id = validate_refresh_token(&req.refresh_token)
        .map_err(|_| StatusError::unauthorized())?;

    // Generate new tokens
    let access_claims = JwtClaims {
        sub: user_id.to_string(),
        exp: (chrono::Utc::now() + chrono::Duration::minutes(15)).timestamp(),
        role: "user".to_string(),
    };

    let access_token = encode(
        &Header::default(),
        &access_claims,
        &EncodingKey::from_secret(SECRET_KEY.as_bytes()),
    )
    .map_err(|_| StatusError::internal_server_error())?;

    // Generate new refresh token and store it
    let refresh_token = generate_refresh_token();

    Ok(Json(TokenResponse {
        access_token,
        refresh_token,
        expires_in: 900, // 15 minutes
    }))
}
rust
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct RefreshRequest {
    refresh_token: String,
}

#[derive(Serialize)]
struct TokenResponse {
    access_token: String,
    refresh_token: String,
    expires_in: i64,
}

#[handler]
async fn refresh_token(body: JsonBody<RefreshRequest>) -> Result<Json<TokenResponse>, StatusError> {
    let req = body.into_inner();

    // 验证刷新令牌(检查数据库/缓存)
    let user_id = validate_refresh_token(&req.refresh_token)
        .map_err(|_| StatusError::unauthorized())?;

    // 生成新令牌
    let access_claims = JwtClaims {
        sub: user_id.to_string(),
        exp: (chrono::Utc::now() + chrono::Duration::minutes(15)).timestamp(),
        role: "user".to_string(),
    };

    let access_token = encode(
        &Header::default(),
        &access_claims,
        &EncodingKey::from_secret(SECRET_KEY.as_bytes()),
    )
    .map_err(|_| StatusError::internal_server_error())?;

    // 生成新的刷新令牌并存储
    let refresh_token = generate_refresh_token();

    Ok(Json(TokenResponse {
        access_token,
        refresh_token,
        expires_in: 900, // 15分钟
    }))
}

Complete Authentication Example

完整认证示例

rust
use salvo::prelude::*;
use salvo::jwt_auth::{ConstDecoder, JwtAuth, HeaderFinder, JwtAuthDepotExt};
use jsonwebtoken::{encode, EncodingKey, Header};
use serde::{Deserialize, Serialize};

const SECRET_KEY: &str = "your-secret-key-at-least-32-bytes";

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

#[derive(Deserialize)]
struct LoginRequest {
    username: String,
    password: String,
}

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

#[handler]
async fn login(body: JsonBody<LoginRequest>) -> Result<Json<LoginResponse>, StatusError> {
    let req = body.into_inner();

    if req.username != "admin" || req.password != "password" {
        return Err(StatusError::unauthorized());
    }

    let claims = JwtClaims {
        sub: req.username,
        exp: (chrono::Utc::now() + chrono::Duration::hours(24)).timestamp(),
    };

    let token = encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(SECRET_KEY.as_bytes()),
    )
    .map_err(|_| StatusError::internal_server_error())?;

    Ok(Json(LoginResponse { token }))
}

#[handler]
async fn protected(depot: &mut Depot) -> Result<String, StatusError> {
    let data = depot.jwt_auth_data::<JwtClaims>()
        .ok_or_else(|| StatusError::unauthorized())?;
    Ok(format!("Welcome, {}!", data.claims.sub))
}

#[tokio::main]
async fn main() {
    let auth: JwtAuth<JwtClaims, _> = JwtAuth::new(ConstDecoder::from_secret(SECRET_KEY.as_bytes()))
        .finders(vec![Box::new(HeaderFinder::new())]);

    let router = Router::new()
        .push(Router::with_path("login").post(login))
        .push(
            Router::with_path("protected")
                .hoop(auth)
                .get(protected)
        );

    let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
    Server::new(acceptor).serve(router).await;
}
rust
use salvo::prelude::*;
use salvo::jwt_auth::{ConstDecoder, JwtAuth, HeaderFinder, JwtAuthDepotExt};
use jsonwebtoken::{encode, EncodingKey, Header};
use serde::{Deserialize, Serialize};

const SECRET_KEY: &str = "your-secret-key-at-least-32-bytes";

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

#[derive(Deserialize)]
struct LoginRequest {
    username: String,
    password: String,
}

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

#[handler]
async fn login(body: JsonBody<LoginRequest>) -> Result<Json<LoginResponse>, StatusError> {
    let req = body.into_inner();

    if req.username != "admin" || req.password != "password" {
        return Err(StatusError::unauthorized());
    }

    let claims = JwtClaims {
        sub: req.username,
        exp: (chrono::Utc::now() + chrono::Duration::hours(24)).timestamp(),
    };

    let token = encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(SECRET_KEY.as_bytes()),
    )
    .map_err(|_| StatusError::internal_server_error())?;

    Ok(Json(LoginResponse { token }))
}

#[handler]
async fn protected(depot: &mut Depot) -> Result<String, StatusError> {
    let data = depot.jwt_auth_data::<JwtClaims>()
        .ok_or_else(|| StatusError::unauthorized())?;
    Ok(format!("欢迎, {}!", data.claims.sub))
}

#[tokio::main]
async fn main() {
    let auth: JwtAuth<JwtClaims, _> = JwtAuth::new(ConstDecoder::from_secret(SECRET_KEY.as_bytes()))
        .finders(vec![Box::new(HeaderFinder::new())]);

    let router = Router::new()
        .push(Router::with_path("login").post(login))
        .push(
            Router::with_path("protected")
                .hoop(auth)
                .get(protected)
        );

    let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
    Server::new(acceptor).serve(router).await;
}

Best Practices

最佳实践

  1. Never store passwords in plain text - use bcrypt or argon2
  2. Use strong secret keys (at least 32 bytes for HMAC, 64 bytes for sessions)
  3. Set appropriate token expiration times (15 min for access, days for refresh)
  4. Use HTTPS in production
  5. Implement rate limiting for login endpoints
  6. Store sensitive data in environment variables
  7. Validate tokens on every protected request
  8. Use refresh tokens for long-lived sessions
  9. Implement proper logout functionality (token revocation)
  10. Log authentication failures for security monitoring
  1. 永远不要明文存储密码 - 使用bcrypt或argon2加密
  2. 使用强密钥(HMAC至少32字节,会话至少64字节)
  3. 设置合理的令牌过期时间(访问令牌15分钟,刷新令牌数天)
  4. 生产环境使用HTTPS
  5. 为登录端点实现速率限制
  6. 敏感数据存储在环境变量中
  7. 每个受保护请求都验证令牌
  8. 使用刷新令牌实现长会话
  9. 实现完善的登出功能(令牌吊销)
  10. 记录认证失败事件用于安全监控