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 чаще всего ломает проекты в трёх сценариях:
Локальная разработка vs. продакшен Локально запрос работает через
http://localhost:3000на фронтенд иhttp://localhost:8000на бэкенд, а в продакшене фронтенд лежит наhttps://frontend.example.com, а бэкенд — наhttps://api.example.com. Заголовки CORS, настроенные для локальной среды, не работают в продакшене.API, доступные для третьих сторон Если ваш бэкенд должен работать не только с вашим фронтендом, но и с мобильным приложением или сторонними сервисами, то CORS становится камнем преткновения. Нужно ли разрешать все домены? Как защитить API от несанкционированного доступа?
Кросс-доменовые запросы с аутентификацией Если вы используете куки или токены в заголовках (
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, убедитесь, что фронтенд и бэкенд используют одинаковые протоколы (
httpvshttps).
Примеры: что может пойти не так
Пример 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: '*'. Нужно явно указать домен.
Типичные ошибки и как их избежать
Использование
origin: '*'в продакшене- Проблема: Открывает API для любых доменов, что опасно с точки зрения безопасности.
- Решение: Указывайте конкретные домены или используйте middleware для динамической проверки.
Забыв про
OPTIONSзапросы- Проблема: Браузер не может выполнить запрос с нестандартными заголовками или методами.
- Решение: Убедитесь, что сервер корректно обрабатывает предварительные запросы.
Неправильные заголовки при использовании аутентификации
- Проблема: Запрос падает с ошибкой о отсутствии
Access-Control-Allow-Credentials. - Решение: Установите
credentials: trueв CORS настройках и убедитесь, что сервер отправляет соответствующий заголовок.
- Проблема: Запрос падает с ошибкой о отсутствии
Разные протоколы (HTTP vs HTTPS)
- Проблема: Фронтенд на
https, а бэкенд наhttp— браузер блокирует запрос. - Решение: Используйте HTTPS везде или настройте CORS для обоих протоколов.
- Проблема: Фронтенд на
Локальная разработка vs продакшен
- Проблема: Настроили CORS для
localhost, а в продакшене фронтенд лежит на другом домене. - Решение: Используйте переменные окружения для динамического указания домена.
- Проблема: Настроили CORS для
Вывод: как не плакать ночью из-за CORS
- Не используйте
origin: '*'в продакшене — это открывает ваш API для всех, кто захочет его использовать. - Всегда проверяйте предварительные запросы (
OPTIONS) — если вы используете нестандартные заголовки или методы, сервер должен корректно отвечать на них. - Для аутентифицированных запросов (
credentials: true) — не забудьте установитьAccess-Control-Allow-Credentials: trueи указать конкретный домен, а не'*'. - Тестируйте CORS в продакшен-подобной среде — локально может работать, а в продакшене — нет из-за разных доменов и протоколов.
- Логируйте ошибки 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 — это не просто набор заголовков, а часть архитектуры вашего приложения. Если вы подходите к нему ответственно, то он не станет проблемой, а будет работать тихо и предсказуемо.