Аутентификация и авторизация
Волга предоставляет гибкие инструменты для реализации аутентификации и авторизации в веб-приложениях. Поддерживаются как базовая аутентификация (Basic Auth), так и токены доступа на основе JWT (Bearer Token). Также доступна система контроля доступа по ролям, разрешениям или пользовательским правилам.
Базовая аутентификация (Basic Auth)
Базовая аутентификация — это простой механизм проверки логина и пароля через HTTP-заголовок Authorization: Basic
.
Зависимости
[dependencies]
volga = { version = "0.6.1", features = ["basic-auth"] }
Пример
use volga::{
App, HttpResult,
headers::WWW_AUTHENTICATE,
auth::Basic,
status, ok
};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.map_get("/protected", protected);
app.run().await
}
async fn protected(auth: Basic) -> HttpResult {
let (expected_user, expected_pass) = get_credentials_from_db().await;
if auth.validate(&expected_user, &expected_pass) {
ok!("Access granted")
} else {
status!(401, "Unauthorized", [
(WWW_AUTHENTICATE, "Basic realm=\"Restricted area\"")
])
}
}
async fn get_credentials_from_db() -> (String, String) {
// В реальном приложении читаем данные из БД или хранилища
("foo".into(), "bar".into())
}
Структура Basic
автоматически извлекает заголовок авторизации и предоставляет методы validate()
и validate_base64()
для проверки логина и пароля.
Аутентификация через JWT (Bearer Token)
JWT (JSON Web Token) предоставляет расширенные возможности: аутентификацию с данными (claims) и авторизацию по ролям и правам доступа.
Зависимости
[dependencies]
volga = { version = "0.6.1", features = ["jwt-auth-full"] }
Генерация токена
use std::{ops::Add, time::{SystemTime, UNIX_EPOCH, Duration}};
use serde::{Serialize, Deserialize};
use volga::{
App, Json, HttpResult,
auth::{Claims, BearerTokenService, EncodingKey},
ok, status, bad_request
};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let secret = std::env::var("JWT_SECRET")
.expect("JWT_SECRET must be set");
let mut app = App::new()
.with_bearer_auth(|auth| {
auth.set_encoding_key(EncodingKey::from_secret(secret.as_bytes()))
});
app.map_post("/generate", generate);
app.run().await
}
async fn generate(payload: Json<Payload>, bts: BearerTokenService) -> HttpResult {
if payload.client_id != "foo" || payload.client_secret != "bar" {
return status!(401, "Invalid credentials");
}
let exp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.add(Duration::from_secs(300))
.as_secs();
let claims = Claims {
sub: "user@email.com".into(),
company: "Awesome Co.".into(),
role: "admin".into(),
exp,
};
let token = bts.encode(&claims)?.to_string();
ok!(AuthData { access_token: token })
}
#[derive(Claims, Serialize, Deserialize)]
struct Claims {
sub: String,
company: String,
role: String,
exp: u64,
}
#[derive(Serialize)]
struct AuthData {
access_token: String,
}
#[derive(Deserialize)]
struct Payload {
client_id: String,
client_secret: String,
}
Использование JWT
Промежуточное ПО authorize() предоставляет инструменты для реализации управления доступом на основе ролей или разрешений и может быть определено для отдельного маршрута, группы маршрутов или всего приложения.
use serde::Deserialize;
use volga::{
App, ok,
auth::{Claims, DecodingKey, roles},
};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let secret = std::env::var("JWT_SECRET")
.expect("JWT_SECRET must be set");
let mut app = App::new()
.with_bearer_auth(|auth| {
auth.set_decoding_key(DecodingKey::from_secret(secret.as_bytes()))
});
app.map_get("/me", me)
.authorize::<Claims>(roles(["admin", "user"]));
app.run().await
}
async fn me() -> &'static str {
"Hello from protected route"
}
#[derive(Claims, Deserialize)]
struct Claims {
sub: String,
company: String,
role: String,
exp: u64,
}
Примечание
Для корректной работы JWT-аутентификации, необходимо использовать один и тот же JWT_SECRET
как при генерации токена, так и при его проверке на защищённых маршрутах. Этот секрет используется для подписания токена на этапе генерации и проверки подписи на этапе валидации. Если секреты отличаются - токен будет отклонён как недействительный.
Определение структуры Claims
Для удобства jwt-auth-full
включает derive-макрос Claims
для объявления структуры claim'ов. Но вы также можете использовать альтернативные способы:
Макрос claims!
use volga::auth::claims;
claims! {
#[derive(Deserialize)]
struct Claims {
sub: String,
role: String,
permissions: Vec<String>,
}
}
Ручная реализация
use volga::auth::AuthClaims;
#[derive(Deserialize)]
struct Claims {
sub: String,
role: String,
permissions: Vec<String>,
}
impl AuthClaims for Claims {
fn role(&self) -> Option<&str> {
Some(&self.role)
}
fn permissions(&self) -> Option<&[String]> {
Some(&self.permissions)
}
}
Декларативный контроль доступа с Authorizer
Волга предоставляет гибкую систему контроля доступа с помощью Authorizer
.
Вы можете проверять доступ по:
role("admin")
- одной роли.roles(["admin", "user"])
- по списку ролей.permission("write")
- по одному разрешению.permissions(["read", "write"])
- по списку разрешений.predicate(|claims| ...)
- кастомная логика.
Пример
use volga::auth::{AuthClaims, role, roles, permission, permissions, predicate, Authorizer};
use serde::Deserialize;
#[derive(Deserialize)]
struct MyClaims {
role: String,
permissions: Vec<String>,
}
impl AuthClaims for MyClaims {
fn role(&self) -> Option<&str> {
Some(&self.role)
}
fn permissions(&self) -> Option<&[String]> {
Some(&self.permissions)
}
}
fn main() {
let admin = role("admin");
let editors = roles(["editor", "contributor"]);
let can_write = permission("write");
let access_policy = admin.or(editors).and(can_write);
let user = MyClaims {
role: "editor".into(),
permissions: vec!["write".into()],
};
assert!(access_policy.validate(&user));
}
Совет
Вы также можете объединять правила в цепочку с помощью комбинаторов and()
и or()
для создания комплексных правил.