Skip to content

Авторизация в Axum

Авторизация - процесс определения прав доступа аутентифицированного пользователя к ресурсам и операциям. В этом разделе мы рассмотрим различные подходы к реализации авторизации в приложениях на Axum.

Содержание

Основные концепции

Авторизация в Axum обычно реализуется с помощью:

  1. Middleware - для глобальной проверки прав доступа
  2. Экстракторов - для получения информации о пользователе и его правах
  3. Обработчиков ошибок - для корректной обработки запретов доступа
  4. Гвардов (guards) - функций или структур для проверки разрешений

Ролевая авторизация

Ролевая авторизация - наиболее распространенный способ управления доступом.

Определение ролей

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

Применение ролевой авторизации в обработчиках

rust
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 для ролевой авторизации

rust
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));

Разрешения и возможности

Для более гибкого контроля доступа можно использовать систему разрешений.

Определение разрешений

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

Проверка разрешений в обработчиках

rust
// Обработчик с проверкой разрешения
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)
    }
}

Создание собственного экстрактора для разрешений

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

Авторизация на уровне ресурсов

Часто требуется авторизация с учетом конкретного ресурса, например, пользователь может редактировать только свои записи.

Проверка владения ресурсом

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

Абстракция для проверки политик доступа

rust
// 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:

rust
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));

Сложные сценарии авторизации

Для сложных сценариев авторизации можно комбинировать различные подходы.

Авторизация с учетом организационной структуры

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

rust
#[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))
}

Лучшие практики

  1. Принцип наименьших привилегий

    • Предоставляйте пользователям только те разрешения, которые им необходимы для выполнения своих задач
    • Начинайте с минимальных прав и добавляйте по мере необходимости
  2. Централизация логики авторизации

    • Выделите логику авторизации в отдельные модули или сервисы
    • Избегайте дублирования проверок в разных частях приложения
  3. Глубокая защита

    • Применяйте авторизацию на разных уровнях: маршруты, обработчики, бизнес-логика
    • Не полагайтесь только на защиту на уровне интерфейса
  4. Аудит и логирование

    rust
    async 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
    }
  5. Разделение ответственности

    • Отделяйте аутентификацию от авторизации
    • Используйте отдельные middleware и экстракторы для каждой задачи
  6. Кэширование разрешений

    • Для повышения производительности кэшируйте результаты проверок авторизации
    • Используйте временные метки для своевременного обновления кэша
    rust
    use 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 предлагает множество инструментов для создания гибкой и надежной системы авторизации, которую можно адаптировать под требования вашего проекта.