Skip to content

CORS в Axum

Cross-Origin Resource Sharing (CORS) — механизм, который позволяет веб-страницам запрашивать данные с других доменов. В этом разделе рассмотрим, как настроить и использовать CORS в приложениях на Axum.

Содержание

Что такое CORS

CORS — это механизм безопасности, который позволяет веб-странице из одного домена (источника) запрашивать и получать данные с сервера другого домена. Без CORS политика безопасности браузера Same-Origin Policy препятствует таким запросам.

Основные компоненты CORS:

  1. Simple requests — простые запросы, которые не требуют предварительной проверки
  2. Preflight requests — предварительные запросы OPTIONS, которые проверяют, разрешен ли настоящий запрос
  3. Заголовки CORS — специальные HTTP заголовки для управления доступом

Настройка CORS с помощью tower-http

Библиотека tower-http предоставляет удобный слой для настройки CORS в приложениях Axum.

Установка зависимостей

toml
[dependencies]
axum = "0.7.2"
tower-http = { version = "0.5.0", features = ["cors"] }

Простая настройка CORS

Простейший способ добавить поддержку CORS в ваше приложение:

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

async fn hello() -> &'static str {
    "Hello, World!"
}

async fn api_endpoint() -> &'static str {
    "{\"message\": \"Это API ответ\"}"
}

#[tokio::main]
async fn main() {
    // Создание слоя CORS с базовыми настройками
    let cors = CorsLayer::new()
        // Разрешить все источники
        .allow_origin(Any)
        // Разрешить все стандартные HTTP-методы
        .allow_methods(Any)
        // Разрешить все стандартные заголовки
        .allow_headers(Any);
        
    // Создание приложения с CORS
    let app = Router::new()
        .route("/", get(hello))
        .route("/api", post(api_endpoint))
        .layer(cors);
        
    // Запуск сервера
    let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Расширенная настройка CORS

Для более детальной настройки CORS:

rust
use axum::{
    http::{HeaderName, HeaderValue, Method},
    routing::{get, post},
    Router,
};
use std::convert::TryFrom;
use tower_http::cors::{AllowOrigin, CorsLayer};

async fn main() {
    // Создание более детальной конфигурации CORS
    let cors = CorsLayer::new()
        // Разрешить только определенные источники
        .allow_origin([
            "https://myapp.com".parse::<HeaderValue>().unwrap(),
            "https://api.myapp.com".parse::<HeaderValue>().unwrap(),
        ])
        // Разрешить только определенные методы
        .allow_methods([Method::GET, Method::POST, Method::PUT])
        // Разрешить только определенные заголовки
        .allow_headers([
            HeaderName::from_static("authorization"),
            HeaderName::from_static("content-type"),
        ])
        // Разрешить кредиталы (cookies, auth headers)
        .allow_credentials(true)
        // Установить максимальный срок кеширования preflight запросов
        .max_age(std::time::Duration::from_secs(3600));
        
    // Создание приложения с настроенным CORS
    let app = Router::new()
        .route("/", get(hello))
        .route("/api", post(api_endpoint))
        .layer(cors);
        
    // Запуск сервера
    let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

CORS для определенных маршрутов

Можно настроить разные политики CORS для разных маршрутов:

rust
use axum::{
    http::{HeaderValue, Method},
    routing::{get, post},
    Router,
};
use tower_http::cors::{CorsLayer, Any};

#[tokio::main]
async fn main() {
    // Создание CORS для публичного API
    let public_cors = CorsLayer::new()
        .allow_origin(Any)
        .allow_methods([Method::GET])
        .allow_headers(Any);
        
    // Создание CORS для административного API
    let admin_cors = CorsLayer::new()
        .allow_origin("https://admin.myapp.com".parse::<HeaderValue>().unwrap())
        .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
        .allow_headers(Any)
        .allow_credentials(true);
        
    // Публичные маршруты с одной политикой CORS
    let public_routes = Router::new()
        .route("/api/public", get(public_api))
        .layer(public_cors);
        
    // Административные маршруты с другой политикой CORS
    let admin_routes = Router::new()
        .route("/api/admin", get(admin_api))
        .layer(admin_cors);
        
    // Объединение маршрутов
    let app = Router::new()
        .merge(public_routes)
        .merge(admin_routes);
        
    // Запуск сервера
    let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 3000));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn public_api() -> &'static str {
    "Это публичный API"
}

async fn admin_api() -> &'static str {
    "Это административный API"
}

Обработка предварительных запросов (preflight)

Preflight запросы (OPTIONS) обрабатываются автоматически слоем CORS. Однако вы можете добавить собственный обработчик для OPTIONS запросов:

rust
use axum::{
    http::{Method, StatusCode},
    routing::{get, on, MethodRouter},
    Router,
};
use tower_http::cors::CorsLayer;

// Обработчик для OPTIONS запросов
async fn options_handler() -> StatusCode {
    StatusCode::NO_CONTENT
}

#[tokio::main]
async fn main() {
    // Настройка CORS
    let cors = CorsLayer::new()
        .allow_origin("https://myapp.com".parse().unwrap())
        .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
        .allow_headers(Any);
        
    // Маршруты с явной обработкой OPTIONS
    let app = Router::new()
        .route("/api",
            get(api_endpoint)
                .options(options_handler)
        )
        .layer(cors);
        
    // Запуск сервера
    // ...
}

Проблемы и их решения

Распространенные проблемы CORS

1. "No 'Access-Control-Allow-Origin' header is present"

Решение:

rust
let cors = CorsLayer::new()
    .allow_origin("https://yourfrontend.com".parse::<HeaderValue>().unwrap())
    // или для разрешения всех источников:
    // .allow_origin(Any)
    .allow_methods(Any);

2. "Method not allowed"

Решение:

rust
let cors = CorsLayer::new()
    .allow_origin(Any)
    .allow_methods([
        Method::GET,
        Method::POST,
        Method::PUT,
        Method::DELETE,
        Method::OPTIONS,
    ]);

3. "Request header field X-Custom-Header is not allowed"

Решение:

rust
let cors = CorsLayer::new()
    .allow_origin(Any)
    .allow_headers([
        HeaderName::from_static("x-custom-header"),
        HeaderName::from_static("content-type"),
        // ...другие необходимые заголовки
    ]);

4. "Credentials is not supported if the CORS header 'Access-Control-Allow-Origin' is '*'"

Решение:

rust
let cors = CorsLayer::new()
    // Необходимо указать конкретные источники при использовании credentials
    .allow_origin("https://yourfrontend.com".parse::<HeaderValue>().unwrap())
    .allow_credentials(true);

Динамическое управление CORS

Для более сложных сценариев вы можете создать собственную функцию для определения CORS политики:

rust
use std::task::{Context, Poll};
use tower::{Layer, Service};
use axum::http::{Request, Response, HeaderValue};
use futures::future::BoxFuture;

// Структура для динамического CORS
struct DynamicCorsLayer;

impl<S> Layer<S> for DynamicCorsLayer {
    type Service = DynamicCorsService<S>;

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

struct DynamicCorsService<S> {
    inner: S,
}

impl<S, B, ReqBody> Service<Request<ReqBody>> for DynamicCorsService<S>
where
    S: Service<Request<ReqBody>, Response = Response<B>> + Clone + Send + 'static,
    S::Future: Send + 'static,
    ReqBody: Send + 'static,
    B: 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, mut req: Request<ReqBody>) -> Self::Future {
        let origin = req
            .headers()
            .get("origin")
            .and_then(|h| h.to_str().ok())
            .map(|s| s.to_string());

        let service = self.inner.clone();
        let mut inner = std::mem::replace(&mut self.inner, service);

        Box::pin(async move {
            let mut response = inner.call(req).await?;

            // Динамически определяем CORS заголовки на основе origin
            if let Some(origin) = origin {
                // Проверка соответствия origin списку разрешенных
                let allowed = is_allowed_origin(&origin);
                
                if allowed {
                    response.headers_mut().insert(
                        "access-control-allow-origin",
                        HeaderValue::from_str(&origin).unwrap(),
                    );
                    
                    response.headers_mut().insert(
                        "access-control-allow-methods",
                        HeaderValue::from_static("GET, POST, PUT, DELETE, OPTIONS"),
                    );
                    
                    response.headers_mut().insert(
                        "access-control-allow-headers",
                        HeaderValue::from_static("content-type, authorization"),
                    );
                    
                    response.headers_mut().insert(
                        "access-control-allow-credentials",
                        HeaderValue::from_static("true"),
                    );
                }
            }

            Ok(response)
        })
    }
}

// Проверка, входит ли origin в список разрешенных
fn is_allowed_origin(origin: &str) -> bool {
    let allowed_origins = [
        "https://myapp.com",
        "https://staging.myapp.com",
        "http://localhost:3000",
    ];
    
    allowed_origins.contains(&origin)
}

Безопасность и CORS

Рекомендации по безопасности

  1. Не используйте allow_origin(Any) для production

    • Всегда указывайте конкретные домены, с которых разрешены запросы
    rust
    let cors = CorsLayer::new()
        .allow_origin([
            "https://myapp.com".parse::<HeaderValue>().unwrap(),
            "https://api.myapp.com".parse::<HeaderValue>().unwrap(),
        ]);
  2. Ограничивайте методы и заголовки

    • Разрешайте только те методы и заголовки, которые действительно используются
    rust
    let cors = CorsLayer::new()
        .allow_methods([Method::GET, Method::POST])
        .allow_headers([
            HeaderName::from_static("content-type"),
            HeaderName::from_static("authorization"),
        ]);
  3. Будьте осторожны с allow_credentials(true)

    • Помните, что это позволяет передавать файлы cookie и заголовки аутентификации
    • Обязательно указывайте конкретные домены при использовании credentials
  4. Используйте подход "белого списка"

    • Явно указывайте разрешенные источники, методы и заголовки
    • Избегайте использования Any в production-окружении
  5. CORS не заменяет другие меры безопасности

    • CORS — это механизм безопасности браузера, он не защищает от запросов, выполняемых вне браузера
    • Всегда используйте дополнительные меры безопасности: аутентификацию, авторизацию, валидацию входных данных

Примеры для разных окружений

Настройка CORS для разработки

rust
// Для локальной разработки
let dev_cors = CorsLayer::new()
    .allow_origin([
        "http://localhost:3000".parse::<HeaderValue>().unwrap(),
        "http://localhost:8080".parse::<HeaderValue>().unwrap(),
    ])
    .allow_methods(Any)
    .allow_headers(Any)
    .allow_credentials(true);

Настройка CORS для production

rust
// Для production
let prod_cors = CorsLayer::new()
    .allow_origin([
        "https://myapp.com".parse::<HeaderValue>().unwrap(),
        "https://www.myapp.com".parse::<HeaderValue>().unwrap(),
    ])
    .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
    .allow_headers([
        HeaderName::from_static("content-type"),
        HeaderName::from_static("authorization"),
    ])
    .allow_credentials(true)
    .max_age(Duration::from_secs(3600));

Правильная настройка CORS критически важна для создания безопасных веб-приложений, особенно тех, которые используют фронтенд, размещенный на отдельном домене от API. Axum с Tower HTTP предоставляет гибкий и удобный способ управления CORS политиками для вашего приложения.