Представьте ситуацию: ваш фронтенд на React пытается отправить данные на бэкенд, написанный на Node.js. Браузер блокирует запрос с ошибкой "No 'Access-Control-Allow-Origin' header is present". Знакомо? Это не баг, а защита. CORS (Cross-Origin Resource Sharing) - это механизм браузера, который контролирует доступ ресурсов из одного домена к ресурсам другого, предотвращая утечку данных через недоверенные источники.
Многие разработчики считают CORS просто "заголовком, который нужно добавить", чтобы всё заработало. Но неправильная настройка может открыть дыру в безопасности размером с дверь. Давайте разберем, как настроить CORS так, чтобы браузер был доволен, а хакеры остались ни с чем.
Как работает механизм проверки Origin
Когда браузер отправляет запрос к другому домену, он добавляет заголовок Origin. Например, если страница находится на https://app.example.com, то в заголовке будет именно этот адрес. Сервер видит этот заголовок и решает, разрешить ли доступ. Если сервер отвечает без соответствующего ответа, браузер молча блокирует чтение ответа, даже если статус кода 200 OK.
Здесь важно понимать разницу между простыми и сложными запросами. Простой запрос - это GET, HEAD или POST с контент-типом text/plain, multipart/form-data или application/x-www-form-urlencoded и без пользовательских заголовков. Для таких запросов браузер не шлет предварительный запрос. Сложный запрос (например, PUT с JSON или GET с кастомным заголовком X-Custom-Header) требует сначала OPTIONS-запрос (preflight), чтобы узнать правила игры.
Ключевые HTTP заголовки и их значения
Чтобы правильно настроить CORS, нужно знать, какие заголовки отправлять. Вот основные:
- Access-Control-Allow-Origin: Указывает, какой источник имеет доступ. Может быть конкретным URL (например,
https://frontend.example.com) или звездочкой (*). Звездочка удобна для публичных API, но опасна, если используются cookies или авторизация. - Access-Control-Allow-Methods: Список разрешенных HTTP методов (GET, POST, PUT, DELETE). Обязателен только для preflight-запросов.
- Access-Control-Allow-Headers: Список разрешенных заголовков для сложных запросов. Если клиент шлет
Authorization, этот заголовок должен быть здесь перечислен. - Access-Control-Max-Age: Время в секундах, сколько браузер кэширует результат preflight-запроса. Уменьшает нагрузку на сервер.
- Vary: Origin: Критически важный заголовок для CDN и прокси. Он говорит кэшу, что ответ зависит от заголовка Origin. Без него разные пользователи могут получить чужие права доступа из кэша.
| Стратегия | Пример значения | Безопасность | Когда использовать |
|---|---|---|---|
| Жесткое ограничение | https://app.mysite.ru | Высокая | Личный кабинет, платежи, приватные данные |
| Динамический эхо | [значение из запроса] | Средняя | Публичные API с авторизацией по токенам |
| Глобальный доступ | * | Низкая (для auth) | Публичные данные без cookies/auth |
Опасность звездочки (*) и Cookies
Использование Access-Control-Allow-Origin: * кажется самым простым решением. Но есть подводный камень. Если вы используете автоматическую отправку cookies (через credentials: include во fetch или withCredentials: true в axios), браузер запрещает использовать звездочку вместе с credentials. В этом случае нужно явно указывать домен источника и добавлять заголовок Access-Control-Allow-Credentials: true.
Если же вы используете Bearer-токены в заголовке Authorization, cookies не участвуют, и звездочка допустима. Однако, если завтра вы решите перейти на session-based authentication, придется менять настройку CORS, иначе все запросы упадут.
Настройка на популярных фреймворках
Рассмотрим практические примеры. В Express.js обычно используют пакет cors. Динамическая проверка выглядит так:
const cors = require('cors');
const allowedOrigins = ['https://app.mysite.ru', 'https://admin.mysite.ru'];
app.use(cors({
origin: function(origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
return callback(null, true);
} else {
return callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
В Django (Python) используется встроенный пакет django-cors-headers. Настройка в settings.py:
CORS_ALLOWED_ORIGINS = [
"https://app.mysite.ru",
"https://staging.mysite.ru",
]
Обратите внимание: в Django лучше избегать CORS_ALLOW_ALL_ORIGINS = True, если у вас есть сессии. Используйте белый список.
Частые ошибки и как их избежать
Даже опытные разработчики попадают в ловушки. Вот три самых распространенных проблемы:
- Забыли про Vary: Origin. Если вы используете Nginx или Cloudflare, убедитесь, что они добавляют
Vary: Origin. Иначе кэш может отдать ответ от одного домена пользователю с другого домена, нарушив изоляцию. - Разное поведение в dev и prod. В локальной разработке часто разрешают
http://localhost:3000. В продакшене этот домен исчезает, и приложение ломается. Всегда тестируйте CORS в среде, близкой к боевой. - Дублирование заголовков. Если middleware в приложении и веб-сервер (Nginx/Apache) оба добавляют CORS-заголовки, браузер увидит дубли и сбросит соединение. Выбирайте одно место для настройки - либо в коде приложения, либо на уровне reverse proxy.
Проверка корректности настройки
Как убедиться, что все настроено верно? Откройте DevTools в браузере, перейдите во вкладку Network. Найдите проблемный запрос. Посмотрите на Response Headers. Там должны быть:
access-control-allow-originс правильным значением.access-control-allow-credentials(если нужны cookies).
Также полезно проверить, нет ли ошибок в консоли браузера. Сообщение "Blocked by CORS policy" всегда указывает на конкретную причину: отсутствие заголовка, несоответствие Origin или запрет credentials со звездочкой.
Безопасность: больше, чем просто заголовки
CORS защищает от чтения чужих ответов, но не заменяет полноценную аутентификацию. Даже если CORS настроен идеально, любой человек может вручную отправить запрос через Postman или curl, минуя браузер. Поэтому CORS - это первый слой защиты, а не последний. Комбинируйте его с проверкой токенов, CSRF-токенами (если используете cookies) и rate limiting.
Помните: CORS решает проблему "браузер не дает прочитать ответ", а не "сервер не знает, кто пришел". Эти две задачи разные, и путать их опасно.