CORS и preflight запросы: механизм работы, заголовки и кэширование
#Веб-разработка #Web API #Веб-безопасностьДля кого эта статья:
- Веб-разработчики и Frontend-специалисты
- DevOps-инженеры и специалисты по веб-безопасности
- Студенты и обучающиеся в области разработки веб-приложений
Разработка кросс-доменных веб-приложений часто превращается в квест по преодолению загадочных ошибок CORS. Сколько раз вы видели в консоли браузера зловещее сообщение "Access to fetch has been blocked by CORS policy"? Да, этот красный текст стал кошмаром многих разработчиков. Но под капотом CORS скрывается чёткий механизм, который защищает пользователей и при правильной настройке не создаёт проблем для легитимных сценариев. Давайте разберём всю механику CORS, углубимся в тайны preflight запросов и научимся оптимизировать их через кэширование. 🔍
CORS: основные принципы политики безопасности браузера
Cross-Origin Resource Sharing (CORS) — это механизм безопасности, реализованный в браузерах для контроля доступа к ресурсам, находящимся на серверах с отличным от текущей страницы происхождением (origin). Origin определяется комбинацией протокола, домена и порта. Например, https://example.com:443.
Без CORS веб-страница с одного домена не могла бы выполнять запросы к API на другом домене из-за политики одинакового источника (Same-Origin Policy, SOP). Эта политика была введена для защиты пользователей от вредоносных скриптов.
Алексей, руководитель отдела веб-безопасности
Мой клиент однажды обратился с проблемой: его приложение, работающее на домене client.example, не могло получить данные от API на домене api.example. Консоль браузера показывала ошибки CORS. Когда я начал расследование, выяснилось, что разработчики клиента не понимали фундаментальную причину: браузер блокировал эти запросы не из вредности, а для защиты пользователей.
Представьте ситуацию: вы авторизованы на банковском сайте, и одновременно открываете вредоносный сайт. Без SOP этот вредоносный сайт мог бы выполнять запросы к вашему банку от вашего имени, используя ваши сохраненные куки. CORS — это не баг, а критически важная функция безопасности.
После настройки корректных CORS-заголовков на стороне API проблема клиента была решена, и этот случай стал образовательным моментом для всей их команды разработки.
CORS позволяет серверам явно указать, каким доменам разрешено обращаться к их ресурсам. Это достигается с помощью специальных HTTP-заголовков в запросе и ответе. Важно понимать, что CORS — это не защита сервера от несанкционированных запросов, а защита пользователя от потенциально опасных действий страниц.
Вот основные принципы, лежащие в основе CORS:
- Запросы из браузера автоматически включают заголовок Origin, указывающий домен, с которого выполняется запрос
- Сервер проверяет этот Origin и решает, разрешить доступ или нет
- Если доступ разрешен, сервер включает в ответ заголовок Access-Control-Allow-Origin
- Браузер проверяет наличие и значение этого заголовка, прежде чем предоставить скрипту доступ к ответу
- Для некоторых "сложных" запросов браузер сначала отправляет preflight (предварительный) запрос методом OPTIONS
| Тип запроса | Характеристика | Требуется preflight |
|---|---|---|
| Простой запрос | GET, HEAD, POST с определенными типами контента | Нет |
| Сложный запрос | PUT, DELETE или POST с нестандартными заголовками | Да |
| Запрос с куками | Любой запрос с credentials: 'include' | Да |
Понимание этих принципов — первый шаг к правильной настройке взаимодействия между разными доменами в вашем веб-приложении. 🔒

Preflight запросы: когда и зачем браузер их отправляет
Preflight запрос (предварительный запрос) — это механизм, который браузер использует для проверки, поддерживает ли сервер определённые HTTP-методы и заголовки перед отправкой фактического запроса. Технически это отдельный HTTP-запрос, выполняемый методом OPTIONS.
Браузер автоматически отправляет preflight запрос в следующих случаях:
- Используется метод отличный от GET, POST или HEAD
- Запрос содержит заголовки, отличные от "безопасных" (например, Authorization, Content-Type со значениями кроме application/x-www-form-urlencoded, multipart/form-data или text/plain)
- При использовании Content-Type, отличного от трёх перечисленных выше
- При наличии пользовательских заголовков (X-Custom-Header)
- При использовании ReadableStream в запросе
- При запросе с включенными credentials в режиме, отличном от same-origin
Preflight запрос содержит несколько ключевых заголовков:
Access-Control-Request-Method: метод, который будет использоваться в фактическом запросеAccess-Control-Request-Headers: список заголовков, которые будут отправлены с фактическим запросомOrigin: источник, с которого выполняется запрос
Сервер должен ответить на preflight запрос, указав, разрешает ли он запрашиваемый метод и заголовки. Это делается с помощью соответствующих заголовков в ответе.
Рассмотрим пример простого preflight запроса:
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, X-Custom-Header
И ответа сервера на него:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, X-Custom-Header
Access-Control-Max-Age: 86400
Только после получения положительного ответа от сервера браузер отправит фактический запрос. Если сервер не предоставит необходимые разрешения, браузер блокирует запрос и выдает ошибку CORS. 🚫
| Заголовок preflight запроса | Описание | Пример |
|---|---|---|
| Access-Control-Request-Method | Метод основного запроса | DELETE |
| Access-Control-Request-Headers | Список нестандартных заголовков | X-Auth-Token, Content-Type |
| Origin | Источник запроса | https://app.example.com |
Важно отметить, что preflight запрос добавляет дополнительную задержку при выполнении кросс-доменных запросов, поскольку требуется дополнительный запрос-ответ перед фактической операцией. Именно поэтому оптимизация и кэширование preflight запросов становятся важными аспектами производительности веб-приложений. ⏱️
Ключевые CORS-заголовки для настройки кросс-доменных запросов
Эффективная настройка CORS требует понимания всего набора заголовков, которые используются как в запросах, так и в ответах. Правильная конфигурация этих заголовков позволяет точно управлять доступом к вашим ресурсам. 🔑
Рассмотрим ключевые CORS-заголовки, которые сервер должен отправлять в ответах:
Access-Control-Allow-Origin: определяет, какие домены могут получать ответы. Может быть конкретным доменом (https://example.com), или, в редких случаях, звездочкой (*) для публичных API.Access-Control-Allow-Methods: список HTTP методов, разрешенных для кросс-доменного доступа (например,GET, POST, PUT, DELETE).Access-Control-Allow-Headers: список заголовков, которые клиент может отправлять в запросе.Access-Control-Allow-Credentials: указывает, может ли запрос включать учетные данные пользователя (cookies, HTTP аутентификацию). Возможные значения:trueили опущено.Access-Control-Expose-Headers: указывает, какие заголовки ответа могут быть доступны скрипту на стороне клиента.Access-Control-Max-Age: максимальное время в секундах, в течение которого результаты preflight запроса могут быть кэшированы.
Дмитрий, DevOps-инженер
В прошлом году к нам обратилась команда разработки мобильного приложения. Их фронтенд, написанный на React Native, постоянно сталкивался с проблемами CORS при обращении к нашему API.
После исследования обнаружилось, что проблема была глубже, чем казалась: они пытались отправлять аутентификационные куки вместе с запросами с мобильного приложения, но не понимали все требования CORS.
Мы настроили следующий набор заголовков:
Access-Control-Allow-Origin: https://app-domain.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Content-Type, Authorization, X-Session-Token Access-Control-Allow-Credentials: true Access-Control-Max-Age: 3600Ключевой инсайт для них: когда используется
Access-Control-Allow-Credentials: true, нельзя использовать wildcard (*) вAccess-Control-Allow-Origin. Это ограничение спецификации CORS для безопасности.После внедрения этих заголовков и переконфигурации клиентского кода проблемы CORS исчезли. Они также обнаружили, что производительность их приложения улучшилась благодаря кэшированию preflight запросов.
Важно понимать взаимосвязь между этими заголовками. Например, если вы устанавливаете Access-Control-Allow-Credentials: true, то Access-Control-Allow-Origin не может быть установлен в * — браузеры требуют явного указания домена в этом случае для защиты пользовательских данных.
При настройке CORS следует придерживаться принципа минимальных привилегий: разрешайте только те домены, методы и заголовки, которые действительно нужны для работы вашего приложения. 🛡️
Типичные ошибки при настройке CORS заголовков:
- Использование
Access-Control-Allow-Origin: *вместе сAccess-Control-Allow-Credentials: true - Слишком ограниченный список в
Access-Control-Allow-Methods, не включающий OPTIONS - Отсутствие необходимых заголовков в
Access-Control-Allow-Headers - Слишком короткое значение
Access-Control-Max-Age, приводящее к частым preflight запросам
Правильная настройка этих заголовков гарантирует, что легитимные клиентские приложения смогут взаимодействовать с вашим API, в то время как потенциально вредоносные запросы будут блокироваться на уровне браузера. 🔐
Кэширование preflight-ответов: оптимизация производительности
Один из наименее понимаемых, но крайне важных аспектов CORS — это кэширование ответов на preflight запросы. Правильная стратегия кэширования может существенно снизить количество OPTIONS запросов, уменьшить нагрузку на сервер и ускорить работу приложения. 🚀
Кэширование preflight-ответов контролируется заголовком Access-Control-Max-Age, который указывает браузеру, как долго (в секундах) можно хранить результат preflight запроса в кэше. По истечении этого времени браузер будет отправлять новый preflight запрос.
Типичное значение Access-Control-Max-Age может варьироваться от нескольких минут до нескольких часов:
Access-Control-Max-Age: 600— кэширование на 10 минутAccess-Control-Max-Age: 3600— кэширование на 1 часAccess-Control-Max-Age: 86400— кэширование на 24 часа
Важно понимать, что у браузеров есть собственные ограничения на максимальное время кэширования preflight ответов, которое может быть меньше указанного в заголовке:
| Браузер | Максимальное значение (секунды) | Максимальное значение (часы) |
|---|---|---|
| Firefox | 86400 | 24 |
| Chrome | 7200 | 2 |
| Safari | 7200 | 2 |
| Edge | 7200 | 2 |
При настройке кэширования preflight-ответов следует учитывать несколько важных факторов:
- Частота изменения CORS политики: если вы часто меняете список разрешенных доменов, методов или заголовков, используйте меньшие значения
Access-Control-Max-Age - Уровень безопасности: для высокозащищенных приложений может быть предпочтительнее более короткое время кэширования
- Интенсивность трафика: для высоконагруженных API более длительное кэширование preflight ответов может значительно снизить нагрузку
- Специфика клиентского приложения: если клиентское приложение часто выполняет разнообразные запросы, длительное кэширование preflight ответов ускорит его работу
Важно отметить, что кэширование происходит на уровне комбинации URL, метода и заголовков. Это означает, что если клиент отправляет запросы к разным эндпоинтам или с разными заголовками, для каждой такой комбинации будет выполнен отдельный preflight запрос. ⚙️
Практический совет: мониторьте количество OPTIONS запросов к вашему API. Если их много, это может указывать на неоптимальное кэширование preflight-ответов или на неоптимальную реализацию клиентской части, которая часто меняет набор заголовков или методов запросов.
Практическая настройка CORS на сервере и обработка OPTIONS
Теоретические знания о CORS важны, но истинная ценность приходит, когда вы можете правильно настроить его на практике. Рассмотрим конкретные примеры настройки CORS для различных серверных технологий. 🛠️
Прежде всего, вне зависимости от используемого сервера или фреймворка, необходимо обеспечить корректную обработку OPTIONS запросов. Это требует:
- Быстрого ответа на OPTIONS запросы (желательно без обращения к базе данных)
- Корректного набора CORS-заголовков в ответе
- Правильного HTTP-статуса (обычно 204 No Content)
Вот примеры настройки CORS для популярных серверных технологий:
1. Node.js с Express:
// Middleware для CORS
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'https://yourdomain.com');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Max-Age', '3600');
// Обработка preflight запросов
if (req.method === 'OPTIONS') {
return res.status(204).end();
}
next();
});
// Или с использованием пакета cors
const cors = require('cors');
app.use(cors({
origin: 'https://yourdomain.com',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
credentials: true,
maxAge: 3600
}));
2. Python с Flask:
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
CORS(app, resources={r"/api/*": {
"origins": "https://yourdomain.com",
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"allow_headers": ["Content-Type", "Authorization", "X-Requested-With"],
"supports_credentials": True,
"max_age": 3600
}})
3. Java с Spring:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://yourdomain.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Content-Type", "Authorization", "X-Requested-With")
.allowCredentials(true)
.maxAge(3600);
}
}
Помимо базовой настройки, следует учитывать ряд практических рекомендаций:
- Динамический Origin: В продакшн-окружении с несколькими фронтендами можно динамически проверять Origin из запроса против белого списка разрешенных доменов.
- Разделение конфигурации по средам: Используйте разные настройки CORS для разработки и продакшена. В разработке можно быть более либеральным, в продакшене — строго ограничивать доступ.
- Мониторинг CORS ошибок: Настройте логирование ошибок CORS, чтобы быстро обнаруживать проблемы с конфигурацией.
- Тестирование CORS-конфигурации: Включите проверку CORS-заголовков в ваши автоматические тесты API.
Типичные проблемы при настройке CORS на сервере:
- Отсутствие CORS-заголовков для ответов с ошибками (4XX, 5XX)
- Конфликты между глобальной настройкой CORS и настройками на уровне отдельных маршрутов
- Недостаточные разрешения для нестандартных заголовков в запросах от фронтенда
- Неправильная обработка запросов с учетными данными (credentials)
Проверить настройку CORS можно с помощью инструментов разработчика в браузере или специализированных инструментов, таких как Postman или curl. Например, с помощью curl можно отправить preflight запрос:
curl -X OPTIONS https://api.example.com/data \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization" \
-v
Тщательная настройка и тестирование CORS конфигурации позволят вашему API эффективно и безопасно взаимодействовать с клиентскими приложениями, при этом соблюдая все требования безопасности современных браузеров. 🔧
CORS и preflight запросы — это не просто технические препятствия, а фундаментальные механизмы безопасности современного веба. Правильное понимание их работы и грамотная настройка позволяют создавать защищённые кросс-доменные приложения без ущерба для производительности. Помните, что неоптимальная обработка preflight запросов может существенно замедлить ваше приложение, а некорректная настройка CORS заголовков может стать причиной серьезных уязвимостей. Инвестируйте время в детальную настройку CORS — это значительно упростит жизнь вашим разработчикам и улучшит пользовательский опыт конечных пользователей.
Элина Баранова
разработчик Android