Skip to content

Middleware в Axum

Middleware в Axum — это компоненты, которые обрабатывают HTTP-запросы до или после основного обработчика. Они позволяют добавлять функциональность, такую как аутентификация, логирование, сжатие и другую бизнес-логику, не дублируя код в каждом обработчике.

Концепция middleware в Axum

В Axum middleware реализуются с помощью нескольких механизмов:

  1. Слои (Layers) — часть экосистемы tower, которая позволяет создавать компоненты промежуточного ПО
  2. Расширения (Extensions) — хранилище типизированных данных, доступных во время обработки запроса
  3. Экстракторы (Extractors) — компоненты для извлечения данных из запроса
  4. Вложенные роутеры (Nested routers) — для применения middleware к определенным группам маршрутов

Основные типы middleware

1. Tower middleware

Tower — это библиотека для построения надежных сетевых сервисов, на которой основана Axum. Tower предоставляет несколько готовых middleware, которые можно использовать в Axum:

rust
use axum::{
    Router,
    routing::get,
};
use tower_http::{
    trace::TraceLayer,
    compression::CompressionLayer,
    timeout::TimeoutLayer,
};
use std::time::Duration;

async fn handler() -> &'static str {
    "Привет, мир!"
}

let app = Router::new()
    .route("/", get(handler))
    // Добавление слоя трассировки для логирования запросов
    .layer(TraceLayer::new_for_http())
    // Добавление автоматического сжатия ответов
    .layer(CompressionLayer::new())
    // Добавление тайм-аута в 30 секунд для всех запросов
    .layer(TimeoutLayer::new(Duration::from_secs(30)));

2. Создание собственного middleware с помощью tower::Service

Вы можете создать собственный middleware, реализовав трейт tower::Service:

rust
use axum::{
    body::Body,
    http::{Request, StatusCode},
    response::{IntoResponse, Response},
    Router,
    routing::get,
};
use futures::future::BoxFuture;
use std::{
    task::{Context, Poll},
    pin::Pin,
};
use tower::{Layer, Service};

// Структура middleware
#[derive(Clone)]
struct AuthMiddleware<S> {
    inner: S,
}

// Реализация Service для AuthMiddleware
impl<S> Service<Request<Body>> for AuthMiddleware<S>
where
    S: Service<Request<Body>, Response = Response> + Clone + Send + 'static,
    S::Future: Send + 'static,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, request: Request<Body>) -> Self::Future {
        // Клонируем сервис, так как мы будем использовать его асинхронно
        let mut inner = self.inner.clone();
        
        Box::pin(async move {
            // Проверяем наличие токена авторизации
            if let Some(auth_header) = request.headers().get("Authorization") {
                if let Ok(auth_str) = auth_header.to_str() {
                    if auth_str.starts_with("Bearer ") {
                        // Токен существует, продолжаем обработку запроса
                        return inner.call(request).await;
                    }
                }
            }
            
            // Если токен отсутствует, возвращаем ошибку авторизации
            Ok(Response::builder()
                .status(StatusCode::UNAUTHORIZED)
                .body(Body::from("Требуется авторизация"))
                .unwrap())
        })
    }
}

// Layer для AuthMiddleware
#[derive(Clone)]
struct AuthLayer;

impl<S> Layer<S> for AuthLayer {
    type Service = AuthMiddleware<S>;

    fn layer(&self, service: S) -> Self::Service {
        AuthMiddleware { inner: service }
    }
}

async fn protected_handler() -> &'static str {
    "Защищенные данные"
}

let app = Router::new()
    .route("/protected", get(protected_handler))
    // Применяем наш middleware аутентификации
    .layer(AuthLayer);

3. Создание простых middleware с помощью функций-обработчиков

Для более простых случаев можно использовать AsyncMiddleware:

rust
use std::time::Instant;
use axum::{
    http::{Request, Response},
    middleware::{self, Next},
    routing::get,
    Router,
    body::Body,
};

// Middleware для измерения времени выполнения запроса
async fn timing_middleware(
    req: Request<Body>,
    next: Next<Body>,
) -> Response {
    let start = Instant::now();
    
    // Передаем запрос следующему обработчику
    let response = next.run(req).await;
    
    // Измеряем время после выполнения
    let duration = start.elapsed();
    println!("Запрос обработан за: {:?}", duration);
    
    response
}

async fn handler() -> &'static str {
    "Привет, мир!"
}

// Создаем маршрутизатор с middleware
let app = Router::new()
    .route("/", get(handler))
    // Применяем middleware ко всем маршрутам
    .layer(middleware::from_fn(timing_middleware));

Применение middleware к группам маршрутов

Вы можете применять middleware только к определенным группам маршрутов:

rust
use axum::{
    Router,
    routing::{get, post},
    middleware,
};

async fn public_handler() -> &'static str {
    "Публичные данные"
}

async fn admin_handler() -> &'static str {
    "Панель администратора"
}

async fn user_handler() -> &'static str {
    "Профиль пользователя"
}

async fn auth_middleware(
    req: Request<Body>,
    next: Next<Body>,
) -> Response {
    // Логика проверки авторизации
    // ...
    next.run(req).await
}

async fn admin_middleware(
    req: Request<Body>,
    next: Next<Body>,
) -> Response {
    // Логика проверки прав администратора
    // ...
    next.run(req).await
}

// Создаем отдельные роутеры
let public_routes = Router::new()
    .route("/", get(public_handler));

let user_routes = Router::new()
    .route("/profile", get(user_handler))
    // Применяем middleware аутентификации только к маршрутам пользователей
    .layer(middleware::from_fn(auth_middleware));

let admin_routes = Router::new()
    .route("/dashboard", get(admin_handler))
    // Применяем middleware администратора
    .layer(middleware::from_fn(admin_middleware))
    // И базовую аутентификацию
    .layer(middleware::from_fn(auth_middleware));

// Объединяем все маршруты
let app = Router::new()
    .nest("/", public_routes)
    .nest("/user", user_routes)
    .nest("/admin", admin_routes);

Расширения (Extensions)

Extensions позволяют передавать данные между middleware и обработчиками:

rust
use axum::{
    extract::Extension,
    middleware::{self, Next},
    response::IntoResponse,
    routing::get,
    Router,
    body::Body,
    http::Request,
};
use std::sync::{Arc, Mutex};

// Структура для хранения статистики
#[derive(Default, Clone)]
struct AppStats {
    request_count: Arc<Mutex<usize>>,
}

// Middleware для подсчета запросов
async fn stats_middleware(
    Extension(stats): Extension<AppStats>,
    req: Request<Body>,
    next: Next<Body>,
) -> impl IntoResponse {
    // Увеличиваем счетчик запросов
    let mut count = stats.request_count.lock().unwrap();
    *count += 1;
    let current_count = *count;
    drop(count);
    
    // Выводим текущее количество запросов
    println!("Запрос #{}", current_count);
    
    // Продолжаем обработку
    next.run(req).await
}

// Обработчик, использующий статистику
async fn stats_handler(Extension(stats): Extension<AppStats>) -> String {
    let count = stats.request_count.lock().unwrap();
    format!("Всего запросов: {}", *count)
}

// Инициализация статистики
let stats = AppStats::default();

// Создаем маршрутизатор
let app = Router::new()
    .route("/", get(|| async { "Привет, мир!" }))
    .route("/stats", get(stats_handler))
    // Добавляем middleware
    .layer(middleware::from_fn_with_state(
        stats.clone(),
        |state, req, next| stats_middleware(Extension(state), req, next)
    ))
    // Добавляем статистику в глобальные расширения
    .layer(Extension(stats));

Порядок выполнения middleware

Порядок добавления middleware важен: они выполняются снаружи внутрь (как луковица):

rust
let app = Router::new()
    .route("/", get(handler))
    .layer(middleware_1) // Выполняется первым
    .layer(middleware_2) // Выполняется вторым
    .layer(middleware_3); // Выполняется третьим

// Порядок выполнения: middleware_3 -> middleware_2 -> middleware_1 -> handler

Обработка ошибок в middleware

В middleware можно перехватывать и обрабатывать ошибки:

rust
use axum::{
    middleware::{self, Next},
    response::IntoResponse,
    routing::get,
    Router,
    body::Body,
    http::{Request, StatusCode},
};
use std::convert::Infallible;

// Обработчик, который может завершиться с ошибкой
async fn fallible_handler() -> Result<&'static str, StatusCode> {
    // Имитация ошибки
    Err(StatusCode::INTERNAL_SERVER_ERROR)
}

// Middleware для обработки ошибок
async fn error_handling_middleware(
    req: Request<Body>,
    next: Next<Body>,
) -> Result<impl IntoResponse, Infallible> {
    // Запускаем следующий обработчик
    let response = next.run(req).await;
    
    // Проверяем статус-код ответа
    if response.status().is_server_error() {
        println!("Произошла серверная ошибка: {}", response.status());
        
        // Логика для уведомления об ошибке (например, отправка в систему мониторинга)
        // ...
        
        // Можно изменить ответ, чтобы показать пользовательское сообщение об ошибке
        return Ok((
            StatusCode::INTERNAL_SERVER_ERROR,
            "Извините, произошла внутренняя ошибка сервера. Наша команда уже работает над этим."
        ).into_response());
    }
    
    // Если ошибки нет, просто передаем исходный ответ
    Ok(response)
}

let app = Router::new()
    .route("/api", get(fallible_handler))
    .layer(middleware::from_fn(error_handling_middleware));

Популярные middleware в экосистеме Axum

CORS (Cross-Origin Resource Sharing)

rust
use axum::{
    Router,
    routing::get,
};
use tower_http::cors::{CorsLayer, Any};

async fn handler() -> &'static str {
    "API-данные"
}

let cors = CorsLayer::new()
    // Разрешаем запросы с любого источника
    .allow_origin(Any)
    // Разрешаем только GET и POST методы
    .allow_methods([http::Method::GET, http::Method::POST])
    // Разрешаем заголовки Content-Type
    .allow_headers([http::header::CONTENT_TYPE]);

let app = Router::new()
    .route("/api", get(handler))
    .layer(cors);

Кэширование

rust
use axum::{
    Router,
    routing::get,
    middleware::{self, Next},
    response::Response,
    body::Body,
    http::{Request, HeaderValue, header},
};

async fn cache_middleware(
    req: Request<Body>,
    next: Next<Body>,
) -> Response {
    let mut response = next.run(req).await;
    
    // Добавляем заголовки кэширования для статических ресурсов
    if req.uri().path().starts_with("/static/") {
        let headers = response.headers_mut();
        headers.insert(
            header::CACHE_CONTROL,
            HeaderValue::from_static("public, max-age=86400"), // 24 часа
        );
    }
    
    response
}

let app = Router::new()
    // Маршруты для статических файлов
    .route("/static/*path", get(serve_static))
    .layer(middleware::from_fn(cache_middleware));

Сжатие

rust
use axum::{
    Router,
    routing::get,
};
use tower_http::compression::{CompressionLayer, CompressionLevel};

let app = Router::new()
    .route("/api/large-data", get(large_data_handler))
    .layer(
        CompressionLayer::new()
            // Настройка поддерживаемых алгоритмов
            .gzip(true)
            .deflate(true)
            .br(true)
            // Задание уровня сжатия
            .level(CompressionLevel::Default)
    );

Ограничение скорости запросов (Rate Limiting)

rust
use axum::{
    extract::Extension,
    middleware::{self, Next},
    response::{IntoResponse, Response},
    routing::get,
    Router,
    body::Body,
    http::{Request, StatusCode},
};
use std::{
    collections::HashMap,
    net::IpAddr,
    sync::{Arc, Mutex},
    time::{Duration, Instant},
};

// Структура для хранения данных о частоте запросов
struct RateLimiter {
    // IP -> (последний запрос, количество запросов)
    clients: Mutex<HashMap<IpAddr, (Instant, usize)>>,
    // Максимальное количество запросов в указанный интервал
    max_requests: usize,
    // Интервал сброса счетчика
    window: Duration,
}

impl RateLimiter {
    fn new(max_requests: usize, window: Duration) -> Self {
        Self {
            clients: Mutex::new(HashMap::new()),
            max_requests,
            window,
        }
    }
    
    // Проверяет, не превышен ли лимит запросов
    fn check(&self, ip: IpAddr) -> bool {
        let mut clients = self.clients.lock().unwrap();
        let now = Instant::now();
        
        // Получаем или создаем запись для IP
        let entry = clients.entry(ip).or_insert((now, 0));
        
        // Если интервал истек, сбрасываем счетчик
        if now.duration_since(entry.0) > self.window {
            *entry = (now, 1);
            return true;
        }
        
        // Увеличиваем счетчик
        entry.1 += 1;
        
        // Проверяем, не превышен ли лимит
        entry.1 <= self.max_requests
    }
}

// Middleware для ограничения частоты запросов
async fn rate_limit_middleware(
    Extension(limiter): Extension<Arc<RateLimiter>>,
    req: Request<Body>,
    next: Next<Body>,
) -> Response {
    // Получаем IP клиента (в реальном приложении учитывайте прокси)
    let ip = req
        .extensions()
        .get::<IpAddr>()
        .cloned()
        .unwrap_or("127.0.0.1".parse().unwrap());
    
    // Проверяем лимит
    if limiter.check(ip) {
        next.run(req).await
    } else {
        // Если лимит превышен, возвращаем ошибку 429
        (
            StatusCode::TOO_MANY_REQUESTS,
            "Слишком много запросов. Пожалуйста, подождите.",
        )
            .into_response()
    }
}

// Создаем лимитер: 100 запросов в минуту
let rate_limiter = Arc::new(RateLimiter::new(
    100,
    Duration::from_secs(60),
));

let app = Router::new()
    .route("/api", get(handler))
    // Добавляем middleware ограничения частоты
    .layer(middleware::from_fn_with_state(
        rate_limiter.clone(),
        |state, req, next| rate_limit_middleware(Extension(state), req, next)
    ))
    // Добавляем лимитер в расширения
    .layer(Extension(rate_limiter));

Создание универсального middleware

Для создания переиспользуемого middleware с конфигурацией можно использовать паттерн Builder:

rust
use axum::{
    middleware::{self, Next},
    response::IntoResponse,
    Router,
    body::Body,
    http::{Request, StatusCode, HeaderValue, header},
};
use std::collections::HashMap;

// Конфигурация заголовков
struct SecurityHeadersConfig {
    headers: HashMap<header::HeaderName, HeaderValue>,
}

impl Default for SecurityHeadersConfig {
    fn default() -> Self {
        let mut headers = HashMap::new();
        
        // Стандартные заголовки безопасности
        headers.insert(
            header::CONTENT_SECURITY_POLICY,
            HeaderValue::from_static("default-src 'self'"),
        );
        headers.insert(
            header::X_FRAME_OPTIONS,
            HeaderValue::from_static("DENY"),
        );
        headers.insert(
            header::X_CONTENT_TYPE_OPTIONS,
            HeaderValue::from_static("nosniff"),
        );
        headers.insert(
            header::STRICT_TRANSPORT_SECURITY,
            HeaderValue::from_static("max-age=31536000; includeSubDomains"),
        );
        
        Self { headers }
    }
}

impl SecurityHeadersConfig {
    // Создает новую конфигурацию
    fn new() -> Self {
        Default::default()
    }
    
    // Добавляет или заменяет заголовок
    fn with_header<K, V>(mut self, key: K, value: V) -> Self
    where
        K: Into<header::HeaderName>,
        V: TryInto<HeaderValue>,
        V::Error: std::fmt::Debug,
    {
        self.headers.insert(
            key.into(),
            value.try_into().expect("Недопустимое значение заголовка"),
        );
        self
    }
    
    // Удаляет заголовок
    fn without_header<K>(mut self, key: K) -> Self
    where
        K: Into<header::HeaderName>,
    {
        self.headers.remove(&key.into());
        self
    }
    
    // Создает middleware из конфигурации
    fn layer(self) -> axum::middleware::from_fn::FromFn<impl Fn(Request<Body>, Next<Body>) -> impl std::future::Future<Output = impl IntoResponse> + Clone> {
        let headers = self.headers;
        
        middleware::from_fn(move |req, next| {
            let headers = headers.clone();
            
            async move {
                let mut response = next.run(req).await;
                
                // Добавляем все заголовки безопасности
                for (key, value) in headers.iter() {
                    response.headers_mut().insert(key, value.clone());
                }
                
                response
            }
        })
    }
}

// Использование
let security_headers = SecurityHeadersConfig::new()
    // Настраиваем CSP для разрешения скриптов с CDN
    .with_header(
        header::CONTENT_SECURITY_POLICY,
        "default-src 'self'; script-src 'self' https://cdn.example.com",
    )
    // Отключаем X-Frame-Options, если не нужен
    .without_header(header::X_FRAME_OPTIONS);

let app = Router::new()
    .route("/", get(handler))
    // Применяем middleware
    .layer(security_headers.layer());

Тестирование middleware

rust
use axum::{
    body::{Body, BoxBody},
    Router,
    middleware::{self, Next},
    http::{Request, Response, StatusCode},
    routing::get,
};
use tower::ServiceExt;
use http_body_util::BodyExt;

// Middleware для проверки авторизации
async fn auth_middleware(
    req: Request<Body>,
    next: Next<Body>,
) -> Response<BoxBody> {
    if let Some(auth_header) = req.headers().get("Authorization") {
        if auth_header == "Bearer valid_token" {
            return next.run(req).await;
        }
    }
    
    Response::builder()
        .status(StatusCode::UNAUTHORIZED)
        .body(BoxBody::default())
        .unwrap()
}

// Тестовый обработчик
async fn test_handler() -> &'static str {
    "Авторизован"
}

// Тесты
#[tokio::test]
async fn test_auth_middleware() {
    // Создаем приложение с middleware
    let app = Router::new()
        .route("/protected", get(test_handler))
        .layer(middleware::from_fn(auth_middleware));
    
    // Тест без токена
    let response = app
        .clone()
        .oneshot(Request::builder()
            .uri("/protected")
            .body(Body::empty())
            .unwrap())
        .await
        .unwrap();
    
    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
    
    // Тест с неверным токеном
    let response = app
        .clone()
        .oneshot(Request::builder()
            .uri("/protected")
            .header("Authorization", "Bearer invalid_token")
            .body(Body::empty())
            .unwrap())
        .await
        .unwrap();
    
    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
    
    // Тест с правильным токеном
    let response = app
        .clone()
        .oneshot(Request::builder()
            .uri("/protected")
            .header("Authorization", "Bearer valid_token")
            .body(Body::empty())
            .unwrap())
        .await
        .unwrap();
    
    assert_eq!(response.status(), StatusCode::OK);
    
    let body = response.into_body().collect().await.unwrap().to_bytes();
    assert_eq!(&body[..], b"Авторизован");
}

Лучшие практики применения middleware

1. Порядок middleware

Располагайте middleware в правильном порядке:

  • Сначала общие middleware (логирование, трассировка)
  • Затем middleware уровня безопасности (CORS, ограничение частоты)
  • Затем бизнес-логика (аутентификация, авторизация)
rust
let app = Router::new()
    .route("/", get(handler))
    // Бизнес-логика
    .layer(middleware::from_fn(auth_middleware))
    // Безопасность
    .layer(CorsLayer::permissive())
    .layer(middleware::from_fn(rate_limit_middleware))
    // Общая функциональность
    .layer(TraceLayer::new_for_http())
    .layer(middleware::from_fn(timing_middleware));

2. Разделение middleware по ответственности

Создавайте middleware для конкретных задач, следуя принципу единственной ответственности:

rust
// Плохо: один middleware делает много задач
async fn complex_middleware(req: Request<Body>, next: Next<Body>) -> Response {
    // Логирование
    println!("Запрос: {:?}", req.uri());
    
    // Авторизация
    if let Some(auth_header) = req.headers().get("Authorization") {
        // ...
    }
    
    // Сбор метрик
    // ...
    
    next.run(req).await
}

// Хорошо: разделение на отдельные middleware
async fn logging_middleware(req: Request<Body>, next: Next<Body>) -> Response {
    println!("Запрос: {:?}", req.uri());
    next.run(req).await
}

async fn auth_middleware(req: Request<Body>, next: Next<Body>) -> Response {
    // Логика авторизации
    // ...
    next.run(req).await
}

async fn metrics_middleware(req: Request<Body>, next: Next<Body>) -> Response {
    // Сбор метрик
    // ...
    next.run(req).await
}

3. Использование расширений для общих данных

rust
// Создаем структуры для общих данных
#[derive(Clone)]
struct DatabasePool(/* ... */);

#[derive(Clone)]
struct AppConfig {
    debug: bool,
    rate_limit: usize,
}

// Применяем как расширения
let db_pool = DatabasePool(/* ... */);
let config = AppConfig {
    debug: true,
    rate_limit: 100,
};

let app = Router::new()
    .route("/", get(handler))
    // Добавляем расширения, доступные во всех middleware и обработчиках
    .layer(Extension(db_pool))
    .layer(Extension(config));

4. Применение middleware к группам маршрутов

rust
// Создаем группы маршрутов с разными middleware
let api_routes = Router::new()
    .route("/users", get(users_handler))
    .route("/posts", get(posts_handler))
    // API-специфичные middleware
    .layer(middleware::from_fn(api_auth_middleware))
    .layer(CorsLayer::permissive());

let admin_routes = Router::new()
    .route("/dashboard", get(dashboard_handler))
    .route("/settings", get(settings_handler))
    // Middleware для админов
    .layer(middleware::from_fn(admin_auth_middleware));

let public_routes = Router::new()
    .route("/", get(home_handler))
    .route("/about", get(about_handler));

// Объединяем маршруты
let app = Router::new()
    .nest("/api", api_routes)
    .nest("/admin", admin_routes)
    .nest("/", public_routes)
    // Общие middleware
    .layer(TraceLayer::new_for_http());

Заключение

Axum предоставляет мощный и гибкий механизм middleware, основанный на экосистеме tower. Основные преимущества:

  1. Совместимость с существующими middleware из экосистемы tower
  2. Возможность создания собственных middleware разной сложности
  3. Гибкая настройка и комбинирование middleware
  4. Типобезопасность благодаря сильной системе типов Rust

При правильном использовании middleware в Axum помогает создавать модульные, поддерживаемые и производительные веб-приложения, избегая дублирования кода и соблюдая принцип разделения ответственности.