Top.Mail.Ru
OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

CORS на практике: правильные заголовки для API и фронта в Nginx и Apache

CORS ломает фронт не из‑за «вредного» браузера, а из‑за неточных заголовков. Практика для Nginx и Apache: простые и preflight‑запросы, белый список Origins, cookies/Authorization, кэширование и отладка.
CORS на практике: правильные заголовки для API и фронта в Nginx и Apache

Когда фронтенд и API живут на разных источниках (домен, поддомен, порт, схема), браузер включает политику CORS. Любая мелочь в заголовках может привести к загадочным ошибкам в консоли и «тихим» отказам. Ниже — практический разбор, как правильно выставить заголовки в Nginx и Apache для реальных сценариев: SPA на одном домене, API на другом, авторизация по cookies/Authorization, несколько доверенных Origins и кэширование preflight.

Что такое CORS и что именно проверяет браузер

CORS — набор правил, по которым браузер решает, можно ли сайту с одного источника (Origin) обращаться к ресурсу на другом источнике. Ключевое: проверка идёт на стороне браузера, а сервер должен отвечать правильными заголовками. Сами запросы всё равно доходят до сервера; именно браузер, увидев «не те» заголовки, блокирует доступ к ответу приложению.

Важно различать два класса запросов:

  • «Простые» (simple): методы GET/HEAD/POST с простыми заголовками и типами контента. Для них нет предварительного запроса, достаточно корректного ответа ресурса с нужными CORS‑заголовками.
  • Preflight (OPTIONS): если метод не простой (PUT, DELETE, PATCH и т. п.) или добавлены пользовательские заголовки (например, Authorization, X-Requested-With), браузер сначала шлёт OPTIONS с заголовками Origin, Access-Control-Request-Method и Access-Control-Request-Headers. Только получив разрешение, он отправит основной запрос.
«Разрешение» — это согласованная связка заголовков: кто именно может обращаться, какими методами, с какими заголовками и можно ли передавать cookies/авторизацию.

Минимальный набор заголовков для разных случаев

Сводно, что обычно требуется:

  • Access-Control-Allow-Origin: конкретный Origin или * (звёздочка). С Credentials звёздочку использовать нельзя.
  • Vary: Origin: чтобы кэш и CDN не отдавали один и тот же ответ разным источникам.
  • Access-Control-Allow-Credentials: true — если нужны cookies/Authorization. Тогда Allow-Origin должен быть конкретным и соответствовать запросу.
  • Для preflight: Access-Control-Allow-Methods, Access-Control-Allow-Headers, опционально Access-Control-Max-Age.
  • Для доступа к нестандартным заголовкам ответа: Access-Control-Expose-Headers.

Белый список Origin в Nginx и обработка preflight OPTIONS

Nginx: надёжная схема с whitelisting Origins

В Nginx удобнее всего разрешать фиксированный список источников, динамически отражая пришедший Origin, если он в белом списке. Это помогает избежать случайного разрешения лишних доменов и корректно работать с cookies. Такой подход типично реализуется на собственных серверах или на VDS с root‑доступом.

Белый список Origins и общие заголовки

map $http_origin $cors_origin {
    default "";
    ~^https?://(www\.)?example\.com$ $http_origin;
    ~^https?://app\.example\.com$ $http_origin;
}

server {
    listen 80;
    server_name api.example.local;

    # Общие CORS-заголовки для обычных запросов
    add_header Access-Control-Allow-Origin $cors_origin always;
    add_header Access-Control-Allow-Credentials true always;
    add_header Vary "Origin" always;

    location /api/ {
        # Для ответа API можно дополнительно раскрыть заголовки
        add_header Access-Control-Expose-Headers "X-Total-Count, ETag" always;

        # Обработка preflight OPTIONS
        if ($request_method = OPTIONS) {
            add_header Access-Control-Allow-Origin $cors_origin always;
            add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
            add_header Access-Control-Allow-Headers "$http_access_control_request_headers" always;
            add_header Access-Control-Allow-Credentials true always;
            add_header Access-Control-Max-Age 86400 always;
            add_header Vary "Origin, Access-Control-Request-Headers, Access-Control-Request-Method" always;
            return 204;
        }

        # Дальше — ваша логика API, например:
        # proxy_pass http://backend;
    }
}

Комментарии к настройке:

  • map безопасно «провалит» пустое значение, если источник не из списка. В этом случае заголовок Access-Control-Allow-Origin просто не будет отправлен.
  • always у add_header гарантирует заголовки не только при 2xx, но и для 3xx/4xx/5xx. Это важно, иначе браузер заблокирует даже корректную ошибку API (например, 401).
  • Вариант с $http_access_control_request_headers отражает запрошенные заголовки в preflight. Для жёсткого контроля замените на белый список, например "Content-Type, Authorization, X-Requested-With".
  • Vary: Origin и в preflight, и в обычных ответах помогает прокси и CDN возвращать корректные варианты для разных источников.

Сценарий без авторизации: разрешить всем

Если API публичный и не использует cookies/Authorization, можно открыть для всех и упростить конфигурацию:

server {
    listen 80;
    server_name public.example.local;

    add_header Access-Control-Allow-Origin * always;
    add_header Vary "Origin" always;

    location /public/ {
        if ($request_method = OPTIONS) {
            add_header Access-Control-Allow-Origin * always;
            add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
            add_header Access-Control-Allow-Headers "$http_access_control_request_headers" always;
            add_header Access-Control-Max-Age 86400 always;
            return 204;
        }

        # proxy_pass http://public-backend;
    }
}

Помните: как только понадобятся cookies или заголовок Authorization, надо отказаться от * и перейти к отражению конкретного Origin плюс добавить Access-Control-Allow-Credentials: true.

Типичные ошибки в Nginx

  • Отсутствует обработка OPTIONS. Браузер делает preflight, получает 405/404 без заголовков и блокирует основной запрос. Лечится явным return 204 с нужными заголовками.
  • Ставите * вместе с Allow-Credentials: true. Браузер игнорирует такой ответ. Нужно отражать конкретный Origin.
  • Нет Vary: Origin. Кэширующие прокси начнут склеивать ответы для разных источников, появятся «мистические» баги.
  • Заголовки только в location, но редиректы/ошибки формируются уровнем выше. Добавляйте ключевые заголовки и на уровне server.
  • 301/302 на preflight. Любой редирект на OPTIONS — почти всегда проблема. Возвращайте 204 прямо на исходный URL.
FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Если вы параллельно наводите порядок с HTTPS и редиректами, пригодится материал про перенос и HSTS — перенос на HTTPS, 301 и HSTS — перенос на HTTPS, 301 и HSTS. Для управления конфигом на сервере через панель посмотрите обзор панелей для VDS — обзор популярных панелей для VDS.

Apache: mod_headers + точечные условия

В Apache оптимальный путь — модуль mod_headers и выражения 2.4 для проверки и отражения Origin. Для preflight полезно отдать 204 и полный набор заголовков. На shared-хостинге часто хватает .htaccess, а на серверах — конфиг виртуального хоста.

Виртуальный хост или .htaccess: whitelisting Origins

# Убедитесь, что загружены модули: headers, rewrite (при необходимости)
# Разрешаем два источника и отражаем пришедший Origin
<If "req('Origin') =~ m#^https?://(www\.)?example\.com$# || req('Origin') =~ m#^https?://app\.example\.com$#">
    Header always set Access-Control-Allow-Origin "%{Origin}i"
    Header always set Access-Control-Allow-Credentials "true"
    Header always merge Vary "Origin"
</If>

# Для конкретного префикса API
<Location "/api/">
    # Разрешаем просматривать определённые заголовки ответа
    Header always set Access-Control-Expose-Headers "X-Total-Count, ETag"

    # Обработка preflight OPTIONS
    <If "req('REQUEST_METHOD') == 'OPTIONS'">
        Header always set Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
        Header always set Access-Control-Allow-Headers "%{Access-Control-Request-Headers}i"
        Header always set Access-Control-Max-Age "86400"
        Header always merge Vary "Origin, Access-Control-Request-Headers, Access-Control-Request-Method"
        Require all granted
        <IfModule mod_rewrite.c>
            RewriteEngine On
            RewriteRule ^ - [R=204]
        </IfModule>
    </If>
</Location>

Пояснения:

  • Header always ... гарантирует отправку заголовков на любые коды ответа.
  • %{Origin}i безопасно отражает пришедший Origin только если условие <If ...> прошло. В противном случае заголовки не выставляются.
  • merge Vary бережно объединяет значения с уже существующим Vary, не перетирая его.
  • В preflight отражаем запрошенные заголовки через %{Access-Control-Request-Headers}i либо подставляем белый список вручную.
  • Ответ на OPTIONS делаем кодом 204, чтобы браузер не воспринимал его как редирект или ошибку.

.htaccess для статической раздачи

Если у вас нет доступа к конфигу виртуального хоста, базовые заголовки можно выставить в .htaccess. Но имейте в виду: логика preflight с кодом 204 через RewriteRule может зависеть от структуры директорий и других правил.

<IfModule mod_headers.c>
    <If "req('Origin') =~ m#^https?://(www\.)?example\.com$#">
        Header always set Access-Control-Allow-Origin "%{Origin}i"
        Header always set Access-Control-Allow-Credentials "true"
        Header always merge Vary "Origin"
    </If>

    <If "req('REQUEST_METHOD') == 'OPTIONS'">
        Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"
        Header always set Access-Control-Allow-Headers "%{Access-Control-Request-Headers}i"
        Header always set Access-Control-Max-Age "600"
    </If>
</IfModule>

Apache mod_headers: отражение Origin и ответ 204 на OPTIONS

Авторизация, cookies и нюансы безопасности

Как только включаете Access-Control-Allow-Credentials: true, вы обязаны:

  • Возвращать точный Access-Control-Allow-Origin, совпадающий с присланным Origin. Никаких *.
  • На фронте использовать fetch с credentials: 'include' или настроить XHR на передачу cookies.
  • Убедиться, что сторонние домены действительно должны иметь доступ к данным пользователя. Подумайте о CSRF и используйте токены/двойные куки.

Частое «облегчение» — отражать Access-Control-Allow-Headers как присланные. Это удобно, но открывает поверхность атак для неожиданных заголовков. В идеале держите белый список и обновляйте его при необходимости.

Кэширование preflight и заголовок Vary

Access-Control-Max-Age позволяет браузеру кэшировать результат preflight. Баланс: от десятков минут до суток. Слишком маленькое значение — лишние OPTIONS, слишком большое — сложнее оперативно отозвать доступ (например, при инциденте). Для публичных API без авторизации разумно ставить 1–24 часа. Для приватных с cookies — консервативнее, 5–30 минут.

Не забывайте про Vary для preflight: запросы отличаются Access-Control-Request-Method и Access-Control-Request-Headers. Добавьте их в Vary, чтобы кэши не путали результаты.

Мульти‑поддомены и шаблоны Origin

Распространённый кейс — нужно открыть доступ для всех поддоменов одного домена. В Nginx используйте регулярку в map, в Apache — выражение expr с регулярным выражением. Важно не допустить подстановок наподобие evil.example.com.attacker.tld: регулярка должна фиксировать границы домена.

Пример для Nginx в map: ~^https?://([a-z0-9-]+\.)*example\.com$. Для Apache аналогичный шаблон в m#^https?://([a-z0-9-]+\.)*example\.com$#.

Диагностика: чем проверять прямо сейчас

Начните с простого curl для preflight и обычного запроса. Смотрите, совпадает ли Access-Control-Allow-Origin с присланным Origin, приходят ли заголовки Allow-Methods/Allow-Headers и нет ли редиректов.

# Preflight на эндпоинт
curl -i -X OPTIONS -H "Origin: https://app.example.com" -H "Access-Control-Request-Method: PUT" -H "Access-Control-Request-Headers: Authorization, Content-Type" https://api.example.com/api/resource

# Обычный запрос с Origin
curl -i -H "Origin: https://app.example.com" https://api.example.com/api/resource

В браузере откройте вкладку Network: у preflight статус должен быть 204 (или 200) без редиректов, а у основного ответа — ожидаемые заголовки CORS. Вкладка Console подскажет конкретное несоответствие заголовков.

Матч‑план внедрения без сюрпризов

  1. Составьте явный белый список Origins и перечень методов/заголовков, которые реально нужны фронту.
  2. Реализуйте настройки сначала на тестовом окружении: Nginx/Apache, preflight с 204, always для заголовков, Vary на месте.
  3. Проверьте сценарии с и без авторизации. Если есть cookies — проверьте, что отклоняется комбинация Allow-Credentials: true и Allow-Origin: * (её быть не должно).
  4. Настройте кэширование preflight: выберите Access-Control-Max-Age и проверьте, как ведут себя промежуточные кэши (если есть CDN/прокси).
  5. Заложите мониторинг: алерты на рост количества OPTIONS, 4xx/5xx на preflight и на основные методы.

Чеклист частых ловушек

  • Забыли Vary: Origin — получаете «рандом» поведение через кэш.
  • Возвращаете 301/302 на OPTIONS — браузер не продолжает основной запрос.
  • Выставляете заголовки только на 2xx — ошибки API становятся невидимыми для фронта.
  • Используете несколько Origins через запятую в Allow-Origin — спецификация запрещает, нужен ровно один.
  • Хардкодите Allow-Headers и забываете про новый заголовок на фронте — preflight начинает падать.
  • Отражаете любые заголовки запроса — удобно, но небезопасно; лучше белый список.

Итоги

Корректная реализация CORS — это не «поставить одну звёздочку». Нужна согласованная конфигурация: точный Access-Control-Allow-Origin, режим с cookies через Allow-Credentials, аккуратная обработка preflight OPTIONS с Allow-Methods/Allow-Headers и кэшированием, плюс Vary для работы с прокси. На практике этого достаточно, чтобы фронт и API стабильно общались между собой, а безопасность и кэширование вели себя предсказуемо.

Если у вас есть нестандартные требования (много доменов, разные политики для путей, сочетание API и статики), разбивайте конфигурацию на зоны ответственности: общий слой по умолчанию и переопределения в нужных локациях/секциях, не забывая про always и Vary. Тогда любые изменения будут локальными и без неожиданных побочных эффектов.

Поделиться статьей

Вам будет интересно

Nginx SSI и подзапросы: сборка страниц из блоков с кэшированием OpenAI Статья написана AI Fastfox

Nginx SSI и подзапросы: сборка страниц из блоков с кэшированием

Практическое руководство по Nginx SSI и subrequest: сборка страницы из блоков, фрагментное кэширование, разделение гостей и автори ...
Мониторинг OPcache: метрики, алерты и быстрая диагностика утечек OpenAI Статья написана AI Fastfox

Мониторинг OPcache: метрики, алерты и быстрая диагностика утечек

OPcache ускоряет PHP, но под нагрузкой может «захлебнуться»: заканчивается память, растёт фрагментация, падает hit rate. Разбираем ...
rclone для больших файлов: multipart‑загрузки, параллелизм и контроль памяти OpenAI Статья написана AI Fastfox

rclone для больших файлов: multipart‑загрузки, параллелизм и контроль памяти

Большие файлы и S3 требуют точной настройки rclone: multipart‑загрузка, параллелизм потоков, контроль памяти и полосы, устойчивост ...