Авторизация в Axum
Авторизация - процесс определения прав доступа аутентифицированного пользователя к ресурсам и операциям. В этом разделе мы рассмотрим различные подходы к реализации авторизации в приложениях на Axum.
Содержание
- Основные концепции
- Ролевая авторизация
- Разрешения и возможности
- Авторизация на уровне ресурсов
- Интеграция с middleware
- Сложные сценарии авторизации
- Лучшие практики
Основные концепции
Авторизация в Axum обычно реализуется с помощью:
- Middleware - для глобальной проверки прав доступа
- Экстракторов - для получения информации о пользователе и его правах
- Обработчиков ошибок - для корректной обработки запретов доступа
- Гвардов (guards) - функций или структур для проверки разрешений
Ролевая авторизация
Ролевая авторизация - наиболее распространенный способ управления доступом.
Определение ролей
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Role {
User,
Moderator,
Admin,
}
impl fmt::Display for Role {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Role::User => write!(f, "user"),
Role::Moderator => write!(f, "moderator"),
Role::Admin => write!(f, "admin"),
}
}
}
impl Role {
// Проверка, является ли роль административной
pub fn is_admin(&self) -> bool {
matches!(self, Role::Admin)
}
// Проверка, является ли роль модераторской или выше
pub fn can_moderate(&self) -> bool {
matches!(self, Role::Admin | Role::Moderator)
}
}
Применение ролевой авторизации в обработчиках
use axum::{
extract::Extension,
http::StatusCode,
response::IntoResponse,
};
// Структура аутентифицированного пользователя с ролью
#[derive(Debug, Clone)]
struct AuthUser {
id: String,
username: String,
role: Role,
}
// Обработчик, доступный только администраторам
async fn admin_only(
Extension(user): Extension<AuthUser>,
) -> Result<impl IntoResponse, StatusCode> {
if user.role.is_admin() {
Ok("Доступ разрешен - это административный раздел")
} else {
Err(StatusCode::FORBIDDEN)
}
}
// Обработчик, доступный модераторам и администраторам
async fn moderator_only(
Extension(user): Extension<AuthUser>,
) -> Result<impl IntoResponse, StatusCode> {
if user.role.can_moderate() {
Ok("Доступ разрешен - это модераторский раздел")
} else {
Err(StatusCode::FORBIDDEN)
}
}
Middleware для ролевой авторизации
use axum::{
http::{Request, StatusCode},
middleware::Next,
response::Response,
};
use std::marker::PhantomData;
// Middleware для проверки ролей
async fn role_guard<B>(
Extension(user): Extension<AuthUser>,
required_role: Extension<Role>,
request: Request<B>,
next: Next<B>,
) -> Result<Response, StatusCode> {
// Проверка соответствия роли
match &user.role {
current if current == required_role.0 => Ok(next.run(request).await),
Role::Admin => Ok(next.run(request).await), // Админ имеет доступ ко всему
Role::Moderator if *required_role.0 == Role::User => Ok(next.run(request).await),
_ => Err(StatusCode::FORBIDDEN),
}
}
// Применение middleware
let app = Router::new()
.route("/admin", get(admin_panel))
.layer(Extension(Role::Admin))
.layer(middleware::from_fn(role_guard))
.route("/moderator", get(moderator_panel))
.layer(Extension(Role::Moderator))
.layer(middleware::from_fn(role_guard));
Разрешения и возможности
Для более гибкого контроля доступа можно использовать систему разрешений.
Определение разрешений
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Permission {
ReadUsers,
CreateUsers,
UpdateUsers,
DeleteUsers,
ReadPosts,
CreatePosts,
UpdatePosts,
DeletePosts,
ModerateComments,
AdminPanel,
}
// Пользователь с разрешениями
#[derive(Debug, Clone)]
struct AuthUser {
id: String,
username: String,
permissions: HashSet<Permission>,
}
impl AuthUser {
// Проверка наличия разрешения
pub fn has_permission(&self, permission: &Permission) -> bool {
self.permissions.contains(permission)
}
// Проверка наличия всех указанных разрешений
pub fn has_all_permissions(&self, permissions: &[Permission]) -> bool {
permissions.iter().all(|p| self.has_permission(p))
}
// Проверка наличия хотя бы одного разрешения
pub fn has_any_permission(&self, permissions: &[Permission]) -> bool {
permissions.iter().any(|p| self.has_permission(p))
}
}
Проверка разрешений в обработчиках
// Обработчик с проверкой разрешения
async fn create_user(
Extension(user): Extension<AuthUser>,
Json(payload): Json<CreateUserRequest>,
) -> Result<impl IntoResponse, StatusCode> {
if user.has_permission(&Permission::CreateUsers) {
// Логика создания пользователя
Ok(StatusCode::CREATED)
} else {
Err(StatusCode::FORBIDDEN)
}
}
// Обработчик с проверкой нескольких разрешений
async fn delete_user(
Extension(user): Extension<AuthUser>,
Path(user_id): Path<String>,
) -> Result<impl IntoResponse, StatusCode> {
if user.has_permission(&Permission::DeleteUsers) {
// Логика удаления пользователя
Ok(StatusCode::NO_CONTENT)
} else {
Err(StatusCode::FORBIDDEN)
}
}
Создание собственного экстрактора для разрешений
#[derive(Debug)]
struct RequirePermission(Permission);
#[async_trait]
impl<B> FromRequest<B> for RequirePermission
where
B: Send,
{
type Rejection = StatusCode;
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// Получение пользователя из расширений запроса
let Extension(user) = Extension::<AuthUser>::from_request(req)
.await
.map_err(|_| StatusCode::UNAUTHORIZED)?;
// Получение требуемого разрешения из расширений запроса
let Extension(permission) = Extension::<Permission>::from_request(req)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Проверка разрешения
if user.has_permission(&permission) {
Ok(RequirePermission(permission))
} else {
Err(StatusCode::FORBIDDEN)
}
}
}
// Использование экстрактора в обработчиках
async fn admin_panel(
_: RequirePermission, // Требуется разрешение AdminPanel
) -> impl IntoResponse {
"Панель администратора"
}
// Настройка маршрутов
let app = Router::new()
.route("/admin", get(admin_panel))
.layer(Extension(Permission::AdminPanel));
Авторизация на уровне ресурсов
Часто требуется авторизация с учетом конкретного ресурса, например, пользователь может редактировать только свои записи.
Проверка владения ресурсом
use axum::{
extract::{Extension, Path},
http::StatusCode,
response::IntoResponse,
};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Post {
id: String,
title: String,
content: String,
author_id: String,
}
// Сервис для работы с постами
#[derive(Clone)]
struct PostService {
// В реальном приложении здесь будет подключение к БД
// ...
}
impl PostService {
// Получение поста по ID
async fn get_post(&self, id: &str) -> Option<Post> {
// Здесь был бы запрос к БД
Some(Post {
id: id.to_string(),
title: "Тестовый пост".to_string(),
content: "Содержание поста".to_string(),
author_id: "user1".to_string(),
})
}
// Обновление поста
async fn update_post(&self, id: &str, title: String, content: String) -> Result<Post, String> {
// Здесь был бы запрос к БД
Ok(Post {
id: id.to_string(),
title,
content,
author_id: "user1".to_string(),
})
}
}
// Структура для обновления поста
#[derive(Debug, Deserialize)]
struct UpdatePostRequest {
title: String,
content: String,
}
// Обработчик с проверкой владения ресурсом
async fn update_post(
Extension(user): Extension<AuthUser>,
Extension(post_service): Extension<PostService>,
Path(post_id): Path<String>,
Json(payload): Json<UpdatePostRequest>,
) -> Result<impl IntoResponse, StatusCode> {
// Получение поста
let post = post_service.get_post(&post_id).await
.ok_or(StatusCode::NOT_FOUND)?;
// Проверка, является ли пользователь автором
if post.author_id != user.id && !user.has_permission(&Permission::UpdatePosts) {
return Err(StatusCode::FORBIDDEN);
}
// Обновление поста
match post_service.update_post(&post_id, payload.title, payload.content).await {
Ok(_) => Ok(StatusCode::OK),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
Абстракция для проверки политик доступа
// Trait для политик авторизации
trait AuthorizationPolicy<T> {
fn authorize(&self, user: &AuthUser, resource: &T) -> bool;
}
// Политика: пользователь может редактировать только свои посты
struct PostOwnerPolicy;
impl AuthorizationPolicy<Post> for PostOwnerPolicy {
fn authorize(&self, user: &AuthUser, post: &Post) -> bool {
user.id == post.author_id || user.has_permission(&Permission::UpdatePosts)
}
}
// Политика: любой пользователь может просматривать опубликованные посты
struct PostViewPolicy;
impl AuthorizationPolicy<Post> for PostViewPolicy {
fn authorize(&self, _user: &AuthUser, post: &Post) -> bool {
post.published || _user.has_permission(&Permission::ReadPosts)
}
}
// Функция для применения политики авторизации
async fn authorize<T, P>(
user: &AuthUser,
resource: &T,
policy: &P,
) -> Result<(), StatusCode>
where
P: AuthorizationPolicy<T>,
{
if policy.authorize(user, resource) {
Ok(())
} else {
Err(StatusCode::FORBIDDEN)
}
}
// Использование в обработчике
async fn edit_post(
Extension(user): Extension<AuthUser>,
Extension(post_service): Extension<PostService>,
Path(post_id): Path<String>,
Json(payload): Json<UpdatePostRequest>,
) -> Result<impl IntoResponse, StatusCode> {
// Получение поста
let post = post_service.get_post(&post_id).await
.ok_or(StatusCode::NOT_FOUND)?;
// Применение политики
authorize(&user, &post, &PostOwnerPolicy).await?;
// Логика обновления поста
// ...
Ok(StatusCode::OK)
}
Интеграция с middleware
Для глобальной авторизации можно использовать middleware:
use axum::{
http::{Request, StatusCode, Method},
middleware::Next,
response::Response,
routing::Route,
};
use std::collections::HashMap;
// Middleware для авторизации на основе пути и метода
async fn path_auth_middleware<B>(
Extension(user): Extension<AuthUser>,
request: Request<B>,
next: Next<B>,
) -> Result<Response, StatusCode> {
// Получение пути и метода
let path = request.uri().path();
let method = request.method();
// Проверка требуемых разрешений для данного пути и метода
let required_permission = match (method, path) {
(&Method::GET, "/users") => Permission::ReadUsers,
(&Method::POST, "/users") => Permission::CreateUsers,
(&Method::PUT, "/users") | (&Method::PATCH, "/users") => Permission::UpdateUsers,
(&Method::DELETE, "/users") => Permission::DeleteUsers,
(&Method::GET, "/posts") => Permission::ReadPosts,
(&Method::POST, "/posts") => Permission::CreatePosts,
(&Method::PUT, "/posts") | (&Method::PATCH, "/posts") => Permission::UpdatePosts,
(&Method::DELETE, "/posts") => Permission::DeletePosts,
(&Method::GET, "/admin") => Permission::AdminPanel,
_ => {
// Путь не требует специального разрешения
return Ok(next.run(request).await);
}
};
// Проверка разрешения
if user.has_permission(&required_permission) {
Ok(next.run(request).await)
} else {
Err(StatusCode::FORBIDDEN)
}
}
// Применение middleware
let app = Router::new()
.route("/users", get(list_users).post(create_user))
.route("/posts", get(list_posts).post(create_post))
.route("/admin", get(admin_panel))
.layer(middleware::from_fn(path_auth_middleware));
Сложные сценарии авторизации
Для сложных сценариев авторизации можно комбинировать различные подходы.
Авторизация с учетом организационной структуры
#[derive(Debug, Clone)]
struct Organization {
id: String,
name: String,
}
#[derive(Debug, Clone)]
struct AuthUser {
id: String,
username: String,
role: Role,
org_id: String,
permissions: HashSet<Permission>,
}
// Сложная логика проверки доступа
async fn check_org_access(
user: &AuthUser,
org_id: &str,
db: &Database,
) -> Result<bool, StatusCode> {
// Проверка, принадлежит ли пользователь к организации
if user.org_id == org_id {
return Ok(true);
}
// Проверка, является ли пользователь глобальным админом
if user.role.is_admin() {
return Ok(true);
}
// Проверка, есть ли у пользователя специальное разрешение
if user.has_permission(&Permission::AccessAllOrgs) {
return Ok(true);
}
// Дополнительные проверки, например, проверка дочерних организаций
let is_parent_org = db.check_parent_organization(user.org_id, org_id).await?;
if is_parent_org {
return Ok(true);
}
Ok(false)
}
// Использование в обработчике
async fn org_data(
Extension(user): Extension<AuthUser>,
Extension(db): Extension<Database>,
Path(org_id): Path<String>,
) -> Result<impl IntoResponse, StatusCode> {
// Проверка доступа
if !check_org_access(&user, &org_id, &db).await? {
return Err(StatusCode::FORBIDDEN);
}
// Получение данных организации
let org_data = db.get_organization_data(&org_id).await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(org_data))
}
Авторизация с разделением данных по арендаторам (multi-tenancy)
#[derive(Debug, Clone)]
struct Tenant {
id: String,
name: String,
}
#[derive(Debug, Clone)]
struct AuthUser {
id: String,
username: String,
tenant_id: String, // ID арендатора
role: Role,
}
// Middleware для проверки доступа к ресурсам арендатора
async fn tenant_middleware<B>(
Extension(user): Extension<AuthUser>,
request: Request<B>,
next: Next<B>,
) -> Result<Response, StatusCode> {
// Извлечение ID арендатора из пути
let path = request.uri().path();
if let Some(tenant_id) = extract_tenant_id(path) {
// Проверка, принадлежит ли пользователь к этому арендатору
if user.tenant_id != tenant_id && !user.role.is_admin() {
return Err(StatusCode::FORBIDDEN);
}
}
// Добавление ID арендатора в расширения запроса
request.extensions_mut().insert(TenantId(user.tenant_id.clone()));
Ok(next.run(request).await)
}
// Извлечение ID арендатора из пути URL
fn extract_tenant_id(path: &str) -> Option<String> {
let parts: Vec<&str> = path.split('/').collect();
if parts.len() >= 3 && parts[1] == "tenants" {
Some(parts[2].to_string())
} else {
None
}
}
// Использование в маршрутах
let app = Router::new()
.route("/tenants/:tenant_id/users", get(list_tenant_users))
.route("/tenants/:tenant_id/resources", get(list_tenant_resources))
.layer(middleware::from_fn(tenant_middleware));
// Обработчик с доступом к ID арендатора
async fn list_tenant_users(
Extension(tenant_id): Extension<TenantId>,
Extension(db): Extension<Database>,
) -> Result<impl IntoResponse, StatusCode> {
let users = db.get_users_by_tenant(&tenant_id.0).await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(users))
}
Лучшие практики
Принцип наименьших привилегий
- Предоставляйте пользователям только те разрешения, которые им необходимы для выполнения своих задач
- Начинайте с минимальных прав и добавляйте по мере необходимости
Централизация логики авторизации
- Выделите логику авторизации в отдельные модули или сервисы
- Избегайте дублирования проверок в разных частях приложения
Глубокая защита
- Применяйте авторизацию на разных уровнях: маршруты, обработчики, бизнес-логика
- Не полагайтесь только на защиту на уровне интерфейса
Аудит и логирование
rustasync fn audit_middleware<B>( Extension(user): Extension<Option<AuthUser>>, request: Request<B>, next: Next<B>, ) -> Response { let start = std::time::Instant::now(); let method = request.method().clone(); let uri = request.uri().clone(); // Получение ID пользователя или "anonymous" let user_id = user.as_ref().map(|u| u.id.clone()).unwrap_or_else(|| "anonymous".to_string()); // Выполнение запроса let response = next.run(request).await; // Логирование let status = response.status(); let duration = start.elapsed(); println!("{} {} {} {} {:?}", method, uri, status, user_id, duration); response }
Разделение ответственности
- Отделяйте аутентификацию от авторизации
- Используйте отдельные middleware и экстракторы для каждой задачи
Кэширование разрешений
- Для повышения производительности кэшируйте результаты проверок авторизации
- Используйте временные метки для своевременного обновления кэша
rustuse std::collections::HashMap; use std::sync::{Arc, RwLock}; use std::time::{Duration, Instant}; // Кэш для результатов авторизации struct AuthorizationCache { cache: HashMap<String, (bool, Instant)>, ttl: Duration, } impl AuthorizationCache { fn new(ttl_seconds: u64) -> Self { Self { cache: HashMap::new(), ttl: Duration::from_secs(ttl_seconds), } } fn get(&self, key: &str) -> Option<bool> { if let Some((result, timestamp)) = self.cache.get(key) { if timestamp.elapsed() < self.ttl { return Some(*result); } } None } fn set(&mut self, key: String, result: bool) { self.cache.insert(key, (result, Instant::now())); } fn clean_expired(&mut self) { self.cache.retain(|_, (_, timestamp)| timestamp.elapsed() < self.ttl); } }
Правильно реализованная авторизация является критически важным компонентом безопасного веб-приложения. Axum предлагает множество инструментов для создания гибкой и надежной системы авторизации, которую можно адаптировать под требования вашего проекта.