CORS: когда браузер становится полицейским

Введение: почему браузер не пускает ваш запрос

Представьте ситуацию: у вас есть фронтенд на frontend.example.com, а бэкенд — на api.example.com. Вы делаете запрос с фронтенда на бэкенд, а браузер отвечает 403 Forbidden с сообщением: "No 'Access-Control-Allow-Origin' header is present". Что пошло не так?

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

Но на практике CORS — это не просто заголовок Access-Control-Allow-Origin. Это целая система, которая включает в себя:

  • Простые запросы (Simple Requests),
  • Предварительные запросы (Preflight Requests),
  • Кредитование (Credentialed Requests),
  • Ошибки и их диагностику.

Если вы не учитываете все эти нюансы, ваш фронтенд будет падать на продакшене, а разработчики будут терять время на поиск причин.


Проблема: когда CORS становится головной болью

CORS чаще всего ломает проекты в трёх сценариях:

  1. Локальная разработка vs. продакшен Локально запрос работает через http://localhost:3000 на фронтенд и http://localhost:8000 на бэкенд, а в продакшене фронтенд лежит на https://frontend.example.com, а бэкенд — на https://api.example.com. Заголовки CORS, настроенные для локальной среды, не работают в продакшене.

  2. API, доступные для третьих сторон Если ваш бэкенд должен работать не только с вашим фронтендом, но и с мобильным приложением или сторонними сервисами, то CORS становится камнем преткновения. Нужно ли разрешать все домены? Как защитить API от несанкционированного доступа?

  3. Кросс-доменовые запросы с аутентификацией Если вы используете куки или токены в заголовках (Authorization), то запрос становится credentialed, и браузер отправляет предварительный запрос (preflight) с дополнительными заголовками. Если сервер не отвечает корректно, запрос падает.


Практика: как настраивать CORS правильно

1. Базовый пример для Node.js (Express)

Если у вас бэкенд на Node.js с Express, то минимальная конфигурация CORS выглядит так:

const express = require('express');
const cors = require('cors');

const app = express();

// Разрешаем запросы только с вашего фронтенда
app.use(cors({
  origin: 'https://frontend.example.com',
  credentials: true // Если нужен доступ к кукам/авторизации
}));

app.get('/api/data', (req, res) => {
  res.json({ data: 'Secret data' });
});

app.listen(8000, () => {
  console.log('Server running on port 8000');
});

Важно:

  • Не используйте origin: '*' в продакшене, если не уверены в безопасности.
  • Если вы используете куки или токены в заголовках, обязательно укажите credentials: true и настройте Access-Control-Allow-Credentials: true на сервере.

2. Настройка CORS для Nginx

Если бэкенд за Nginx, то CORS настраивается через заголовки:

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

    location / {
        proxy_pass http://backend_server;
        add_header 'Access-Control-Allow-Origin' 'https://frontend.example.com';
        add_header 'Access-Control-Allow-Credentials' 'true';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
    }
}

Обратите внимание:

  • Для предварительных запросов (OPTIONS) сервер должен отвечать 204 No Content или 200 OK с корректными заголовками.
  • Если вы используете HTTPS, убедитесь, что фронтенд и бэкенд используют одинаковые протоколы (http vs https).

Примеры: что может пойти не так

Пример 1: Запрос падает из-за отсутствия предварительного запроса (Preflight)

Если вы делаете запрос с нестандартными заголовками (например, X-Custom-Header) или нестандартным методом (например, PUT), браузер сначала отправит OPTIONS запрос, чтобы уточнить у сервера, можно ли это делать.

Проблема: Сервер не отвечает на OPTIONS корректно или не отправляет нужные заголовки.

Решение: Убедитесь, что сервер обрабатывает OPTIONS запросы и возвращает:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Custom-Header
Access-Control-Max-Age: 86400

Пример 2: Ошибка "No 'Access-Control-Allow-Credentials' header is present"

Если вы используете куки или токены в заголовках (Authorization), то запрос считается credentialed, и браузер требует явного разрешения.

Проблема: Сервер не отправляет Access-Control-Allow-Credentials: true.

Решение: В Express:

app.use(cors({
  origin: 'https://frontend.example.com',
  credentials: true
}));

В Nginx:

add_header 'Access-Control-Allow-Credentials' 'true';

Важно: Если вы используете credentials: true, то нельзя использовать origin: '*'. Нужно явно указать домен.


Типичные ошибки и как их избежать

  1. Использование origin: '*' в продакшене

    • Проблема: Открывает API для любых доменов, что опасно с точки зрения безопасности.
    • Решение: Указывайте конкретные домены или используйте middleware для динамической проверки.
  2. Забыв про OPTIONS запросы

    • Проблема: Браузер не может выполнить запрос с нестандартными заголовками или методами.
    • Решение: Убедитесь, что сервер корректно обрабатывает предварительные запросы.
  3. Неправильные заголовки при использовании аутентификации

    • Проблема: Запрос падает с ошибкой о отсутствии Access-Control-Allow-Credentials.
    • Решение: Установите credentials: true в CORS настройках и убедитесь, что сервер отправляет соответствующий заголовок.
  4. Разные протоколы (HTTP vs HTTPS)

    • Проблема: Фронтенд на https, а бэкенд на http — браузер блокирует запрос.
    • Решение: Используйте HTTPS везде или настройте CORS для обоих протоколов.
  5. Локальная разработка vs продакшен

    • Проблема: Настроили CORS для localhost, а в продакшене фронтенд лежит на другом домене.
    • Решение: Используйте переменные окружения для динамического указания домена.

Вывод: как не плакать ночью из-за CORS

  1. Не используйте origin: '*' в продакшене — это открывает ваш API для всех, кто захочет его использовать.
  2. Всегда проверяйте предварительные запросы (OPTIONS) — если вы используете нестандартные заголовки или методы, сервер должен корректно отвечать на них.
  3. Для аутентифицированных запросов (credentials: true) — не забудьте установить Access-Control-Allow-Credentials: true и указать конкретный домен, а не '*'.
  4. Тестируйте CORS в продакшен-подобной среде — локально может работать, а в продакшене — нет из-за разных доменов и протоколов.
  5. Логируйте ошибки CORS — если запрос падает, посмотрите в консоль браузера, какой именно заголовок отсутствует.

Практический совет: Создайте скрипт для проверки CORS на вашем сервере. Например, с помощью curl:

curl -X OPTIONS \
  -H "Origin: https://frontend.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type,authorization" \
  http://api.example.com/api/data \
  -v

Если сервер отвечает корректно, то CORS настроен правильно. Если нет — ищите ошибку в конфигурации.

CORS — это не просто набор заголовков, а часть архитектуры вашего приложения. Если вы подходите к нему ответственно, то он не станет проблемой, а будет работать тихо и предсказуемо.