salvo-auth

📁 salvo-rs/salvo-skills 📅 3 days ago
1
总安装量
1
周安装量
#45263
全站排名
安装命令
npx skills add https://github.com/salvo-rs/salvo-skills --skill salvo-auth

Agent 安装分布

amp 1
opencode 1
cursor 1
kimi-cli 1
github-copilot 1

Skill 文档

Salvo Authentication

This skill helps implement authentication and authorization in Salvo applications.

JWT Authentication

Setup

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

JWT Middleware Setup

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

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 }))
}

Protected Handler

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

Setup

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

Basic Auth Middleware

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

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(())
    }
}

API Key Authentication

#[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"
}

Session-Based Authentication

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()),
    }
}

Role-Based Access Control (RBAC)

#[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)
    );

JWT with Refresh Tokens

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
    }))
}

Complete Authentication Example

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;
}

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