一键导入
salvo-auth
Implement authentication and authorization using JWT, Basic Auth, or custom schemes. Use for securing API endpoints and user management.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Implement authentication and authorization using JWT, Basic Auth, or custom schemes. Use for securing API endpoints and user management.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Create basic Salvo web applications with handlers, routers, and server setup. Use when starting a new Salvo project or adding basic HTTP endpoints.
Implement caching strategies for improved performance. Use for reducing database load and speeding up responses.
Compress HTTP responses using gzip, brotli, zstd, or deflate. Use for reducing bandwidth and improving load times.
Limit concurrent requests to protect resources. Use for file uploads, expensive operations, and preventing resource exhaustion.
Configure Cross-Origin Resource Sharing (CORS) and security headers. Use for APIs accessed from browsers on different domains.
Implement CSRF (Cross-Site Request Forgery) protection using cookie or session storage. Use for protecting forms and state-changing endpoints.
| name | salvo-auth |
| description | Implement authentication and authorization using JWT, Basic Auth, or custom schemes. Use for securing API endpoints and user management. |
| version | 0.94.0 |
| tags | ["security","authentication","jwt","basic-auth"] |
[dependencies]
salvo = { version = "0.94.0", features = ["jwt-auth"] }
jsonwebtoken = "9"
serde = { version = "1", features = ["derive"] }
chrono = "0.4"
JwtAuth::new takes a decoder (not a raw secret string). Use ConstDecoder::from_secret for HMAC or ConstDecoder::from_rsa_pem / from_ec_pem for asymmetric keys.
use salvo::prelude::*;
use salvo::jwt_auth::{ConstDecoder, HeaderFinder, JwtAuth, JwtAuthDepotExt, QueryFinder};
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,
role: String,
}
#[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(),
role: "user".to_string(),
};
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!("Hello, {}! Role: {}", data.claims.sub, data.claims.role))
}
#[tokio::main]
async fn main() {
let auth: JwtAuth<JwtClaims, _> = JwtAuth::new(ConstDecoder::from_secret(SECRET_KEY.as_bytes()))
.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).get(protected));
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
Available finders: HeaderFinder, QueryFinder, FormFinder, CookieFinder. Available decoders include ConstDecoder and (with oidc feature) OidcDecoder.
JwtAuthDepotExt provides:
depot.jwt_auth_token() -> Option<&str>depot.jwt_auth_data::<C>() -> Option<&TokenData<C>>depot.jwt_auth_state() -> JwtAuthState (Authorized / Unauthorized / Forbidden)[dependencies]
salvo = { version = "0.94.0", features = ["basic-auth"] }
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 {
false
}
}
}
#[tokio::main]
async fn main() {
let auth = BasicAuth::new(MyValidator);
let router = Router::with_path("admin").hoop(auth).get(admin_handler);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
BasicAuth::new(v).set_realm("myapp") customizes the realm. Retrieve the authenticated user via depot.basic_auth_username() (BasicAuthDepotExt).
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.and_then(|t| validate_token(&t).ok()) {
Some(user_id) => {
depot.insert("user_id", user_id);
}
None => {
res.status_code(StatusCode::UNAUTHORIZED);
res.render(Json(serde_json::json!({"error": "Unauthorized"})));
ctrl.skip_rest();
}
}
}
fn validate_token(token: &str) -> Result<i64, ()> {
if token == "valid_token" { Ok(123) } else { Err(()) }
}
For an API key scheme, read req.header::<String>("X-API-Key") and follow the same pattern.
#[derive(Clone, PartialEq)]
enum Role { Admin, User, Guest }
fn require_role(required: Role) -> impl Handler {
#[handler]
async fn check(depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
// Pull required role from a closure-captured value in real code.
}
check
}
In practice, store the role in Depot during authentication, then build a small #[handler] that reads it and calls ctrl.skip_rest() with 403 on mismatch. Chain it after the auth middleware:
Router::with_path("admin")
.hoop(auth)
.hoop(require_admin)
.get(admin_handler);
Issue a short-lived access token (e.g. 15 min) and a long-lived refresh token stored server-side (DB/Redis). On /refresh, validate the refresh token, look up the user, mint a new access token, and optionally rotate the refresh token.
#[handler]
async fn refresh(body: JsonBody<RefreshRequest>) -> Result<Json<TokenResponse>, StatusError> {
let user_id = validate_refresh_token(&body.into_inner().refresh_token)
.map_err(|_| StatusError::unauthorized())?;
let claims = JwtClaims {
sub: user_id.to_string(),
exp: (chrono::Utc::now() + chrono::Duration::minutes(15)).timestamp(),
role: "user".into(),
};
let access_token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(SECRET_KEY.as_bytes()),
).map_err(|_| StatusError::internal_server_error())?;
Ok(Json(TokenResponse {
access_token,
refresh_token: generate_refresh_token(),
expires_in: 900,
}))
}
JwtAuth::new requires a JwtAuthDecoder impl, not a &str. Passing a string will not compile.JwtAuth has a type parameter for the claims type; annotate it (JwtAuth<JwtClaims, _>) when the compiler cannot infer it..force_passed(true) lets the request continue even when unauthorized so downstream handlers can decide; default is false.BasicAuthValidator::validate uses native async fn in traits (no async_trait crate required).