CORS (Cross-Origin Resource Sharing) — механизм безопасности браузера, основанный на Same-Origin Policy (SOP). SOP запрещает странице делать запросы к другому "origin" (комбинации протокол+домен+порт). CORS позволяет серверу явно разрешить запросы с определённых чужих origins через специальные HTTP-заголовки. Важно: CORS ограничивает только браузер — серверные запросы (Node.js, curl) SOP не затрагивает.
"Origin" — это комбинация из трёх частей:
https://example.com:443/path
└─── протокол ───┘ └── домен ──┘ └─ порт ─┘
https example.com 443
Всё вместе = один OriginПравило: браузер разрешает скрипту делать запросы только к **тому же origin**:
Страница: https://myapp.com
РАЗРЕШЕНО:
https://myapp.com/api/users ← тот же origin
https://myapp.com/data.json ← тот же origin
ЗАБЛОКИРОВАНО браузером:
https://api.other.com/users ← другой домен
http://myapp.com/api ← другой протокол (http vs https)
https://myapp.com:8080/api ← другой портСервер добавляет заголовки, которые говорят браузеру: "я разрешаю запросы от этих origins":
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, AuthorizationЕсли заголовок есть и origin совпадает — браузер разрешает доступ к ответу.
**Простые запросы** (simple requests) — браузер делает сразу без preflight:
Методы: GET, POST, HEAD
Заголовки: только стандартные (Content-Type: text/plain, application/x-www-form-urlencoded, multipart/form-data)**Непростые запросы** — перед основным запросом браузер делает **preflight** (предзапрос):
Триггеры для preflight:
- Методы: PUT, DELETE, PATCH
- Заголовки: Authorization, Content-Type: application/json, кастомные
- Тело запроса в JSONБраузер → Сервер:
OPTIONS /api/users HTTP/1.1
Origin: https://myapp.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization, Content-Type
Сервер → Браузер:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400 ← кешировать preflight 24 часа
Браузер → Сервер (основной запрос):
DELETE /api/users/42 HTTP/1.1
Origin: https://myapp.com
Authorization: Bearer token123По умолчанию CORS запросы не включают куки и заголовки авторизации. Для их включения:
// На клиенте
fetch('https://api.example.com/data', {
credentials: 'include' // отправлять куки
})
// На сервере (нельзя использовать * с credentials!)
Access-Control-Allow-Origin: https://myapp.com // конкретный origin, не *
Access-Control-Allow-Credentials: true1. Правильная настройка сервера (лучший вариант)
// Express.js пример
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
if (req.method === 'OPTIONS') {
res.status(204).end()
return
}
next()
})**2. Прокси-сервер** — браузер → твой сервер → чужой API (CORS не применяется к серверным запросам):
Браузер → myapp.com/api/proxy → api.other.com
↑ тот же origin ↑ серверный запрос, SOP не работает**3. JSONP** (устаревший) — только GET, использует <script src> который не подпадает под SOP.
// CORS нельзя обойти из браузера:
// - Изменить заголовки запроса чтобы "притвориться" другим origin
// - Читать ответ без разрешения сервера
// - Использовать fetch с другим Origin заголовком
// Это работает только не в браузере (curl, Node.js, Postman):
// curl -H "Origin: https://evil.com" https://api.example.com/data
// ← сервер ответит, но браузер бы заблокировалCORS — защита **браузера** от атак типа CSRF, где злой сайт пытается сделать запрос к твоему банку от имени твоего браузера.
Access-Control-Allow-Origin — разрешённые origins (* или конкретный)
Access-Control-Allow-Methods — разрешённые HTTP методы
Access-Control-Allow-Headers — разрешённые заголовки запроса
Access-Control-Expose-Headers — заголовки ответа, доступные JS
Access-Control-Allow-Credentials — разрешить куки/авторизацию
Access-Control-Max-Age — время кеширования preflight (секунды)**Начни с SOP**: "CORS существует из-за Same-Origin Policy — браузер блокирует запросы к другому origin в целях безопасности."
**Объясни механизм**: "CORS позволяет серверу указать, каким origins он доверяет, через специальные заголовки ответа."
**Упомяни preflight**: "Для непростых запросов (DELETE, JSON-тело, кастомные заголовки) браузер сначала отправляет OPTIONS preflight-запрос."
**Скажи как решать**: "Правильное решение — настроить CORS-заголовки на сервере. Прокси — альтернатива когда нет доступа к серверу."
**Подчеркни безопасность**: "CORS нельзя обойти из браузера — это защита от CSRF. Серверные запросы SOP не затрагивает."
**Время ответа**: 3-4 минуты.
1. **"CORS — это ошибка которую надо отключить"** или **"просто добавь Access-Control-Allow-Origin: *"** — это игнорирование модели безопасности. Wildcard * нельзя использовать с credentials и это потенциально небезопасно.
2. **"CORS обходится на клиенте"** — нет. CORS — защита браузера. Обойти можно только через прокси на своём сервере, но не из браузерного JS.
3. **Не знать о preflight** — если не понимаешь почему DELETE или POST с JSON вызывает дополнительный OPTIONS-запрос — ты не понимаешь как CORS работает на практике.
Демонстрация простых vs непростых запросов, логика preflight и правильная диагностика CORS-ошибок
// ===== ЧТО ДЕЛАЕТ ЗАПРОС "НЕПРОСТЫМ" (требует preflight) =====
console.log('=== Simple vs Non-Simple Requests ===')
function analyzeRequest(config) {
const { method, headers = {}, body } = config
const simpleMethods = ['GET', 'POST', 'HEAD']
const simpleContentTypes = [
'application/x-www-form-urlencoded',
'multipart/form-data',
'text/plain'
]
const simpleHeaders = [
'accept', 'accept-language', 'content-language', 'content-type'
]
const issues = []
// Проверяем метод
if (!simpleMethods.includes(method.toUpperCase())) {
issues.push(`Метод ${method} не простой (только GET, POST, HEAD)`)
}
// Проверяем Content-Type
const ct = headers['Content-Type'] || headers['content-type'] || ''
if (ct && !simpleContentTypes.some(t => ct.toLowerCase().startsWith(t))) {
issues.push(`Content-Type "${ct}" не простой`)
}
// Проверяем кастомные заголовки
const customHeaders = Object.keys(headers).filter(
h => !simpleHeaders.includes(h.toLowerCase()) && h.toLowerCase() !== 'content-type'
)
if (customHeaders.length > 0) {
issues.push(`Кастомные заголовки: ${customHeaders.join(', ')}`)
}
const needsPreflight = issues.length > 0
return { needsPreflight, reasons: issues }
}
const requests = [
{ label: 'Простой GET', config: { method: 'GET' } },
{ label: 'Простой POST (form)', config: { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } },
{ label: 'POST с JSON', config: { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' } },
{ label: 'POST с Authorization', config: { method: 'POST', headers: { 'Authorization': 'Bearer token' } } },
{ label: 'DELETE запрос', config: { method: 'DELETE' } },
{ label: 'PUT с JSON и Auth', config: { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer token' } } },
]
for (const { label, config } of requests) {
const result = analyzeRequest(config)
const status = result.needsPreflight ? '⚡ PREFLIGHT нужен' : 'OK простой'
console.log(`\n${label}: ${status}`)
if (result.reasons.length > 0) {
result.reasons.forEach(r => console.log(` - ${r}`))
}
}
// ===== СИМУЛЯЦИЯ PREFLIGHT ПОТОКА =====
console.log('\n=== Симуляция Preflight потока ===')
function simulateCorsFlow(clientOrigin, request, serverConfig) {
console.log(`\nКлиент: ${clientOrigin}`)
console.log(`Запрос: ${request.method} ${request.url}`)
const { needsPreflight } = analyzeRequest(request)
if (needsPreflight) {
console.log('\n→ Браузер отправляет OPTIONS preflight:')
console.log(` OPTIONS ${request.url}`)
console.log(` Origin: ${clientOrigin}`)
console.log(` Access-Control-Request-Method: ${request.method}`)
if (request.headers) {
console.log(` Access-Control-Request-Headers: ${Object.keys(request.headers).join(', ')}`)
}
const allowed = serverConfig.allowedOrigins.includes(clientOrigin) ||
serverConfig.allowedOrigins.includes('*')
const methodAllowed = serverConfig.allowedMethods.includes(request.method)
if (allowed && methodAllowed) {
console.log('\n← Сервер отвечает на preflight:')
console.log(` HTTP/1.1 204 No Content`)
console.log(` Access-Control-Allow-Origin: ${clientOrigin}`)
console.log(` Access-Control-Allow-Methods: ${serverConfig.allowedMethods.join(', ')}`)
console.log(` Access-Control-Allow-Headers: ${serverConfig.allowedHeaders.join(', ')}`)
console.log(` Access-Control-Max-Age: 86400`)
console.log('\n→ Preflight прошёл! Отправляем основной запрос...')
console.log('← Основной запрос выполнен успешно')
} else {
console.log(`\n← Сервер отвечает на preflight: заблокировано!`)
if (!allowed) console.log(` Origin ${clientOrigin} не разрешён`)
if (!methodAllowed) console.log(` Метод ${request.method} не разрешён`)
console.log('← Браузер блокирует основной запрос → CORS Error')
}
} else {
console.log('→ Простой запрос — без preflight')
const allowed = serverConfig.allowedOrigins.includes(clientOrigin) ||
serverConfig.allowedOrigins.includes('*')
console.log(allowed
? '← Сервер разрешает: Access-Control-Allow-Origin присутствует'
: '← Сервер не отправил CORS заголовки → браузер блокирует')
}
}
const serverConfig = {
allowedOrigins: ['https://myapp.com', 'https://staging.myapp.com'],
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID']
}
simulateCorsFlow('https://myapp.com', {
method: 'DELETE',
url: 'https://api.backend.com/users/42',
headers: { 'Authorization': 'Bearer token' }
}, serverConfig)
simulateCorsFlow('https://evil.com', {
method: 'POST',
url: 'https://api.backend.com/users',
headers: { 'Content-Type': 'application/json' }
}, serverConfig)CORS (Cross-Origin Resource Sharing) — механизм безопасности браузера, основанный на Same-Origin Policy (SOP). SOP запрещает странице делать запросы к другому "origin" (комбинации протокол+домен+порт). CORS позволяет серверу явно разрешить запросы с определённых чужих origins через специальные HTTP-заголовки. Важно: CORS ограничивает только браузер — серверные запросы (Node.js, curl) SOP не затрагивает.
"Origin" — это комбинация из трёх частей:
https://example.com:443/path
└─── протокол ───┘ └── домен ──┘ └─ порт ─┘
https example.com 443
Всё вместе = один OriginПравило: браузер разрешает скрипту делать запросы только к **тому же origin**:
Страница: https://myapp.com
РАЗРЕШЕНО:
https://myapp.com/api/users ← тот же origin
https://myapp.com/data.json ← тот же origin
ЗАБЛОКИРОВАНО браузером:
https://api.other.com/users ← другой домен
http://myapp.com/api ← другой протокол (http vs https)
https://myapp.com:8080/api ← другой портСервер добавляет заголовки, которые говорят браузеру: "я разрешаю запросы от этих origins":
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, AuthorizationЕсли заголовок есть и origin совпадает — браузер разрешает доступ к ответу.
**Простые запросы** (simple requests) — браузер делает сразу без preflight:
Методы: GET, POST, HEAD
Заголовки: только стандартные (Content-Type: text/plain, application/x-www-form-urlencoded, multipart/form-data)**Непростые запросы** — перед основным запросом браузер делает **preflight** (предзапрос):
Триггеры для preflight:
- Методы: PUT, DELETE, PATCH
- Заголовки: Authorization, Content-Type: application/json, кастомные
- Тело запроса в JSONБраузер → Сервер:
OPTIONS /api/users HTTP/1.1
Origin: https://myapp.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization, Content-Type
Сервер → Браузер:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400 ← кешировать preflight 24 часа
Браузер → Сервер (основной запрос):
DELETE /api/users/42 HTTP/1.1
Origin: https://myapp.com
Authorization: Bearer token123По умолчанию CORS запросы не включают куки и заголовки авторизации. Для их включения:
// На клиенте
fetch('https://api.example.com/data', {
credentials: 'include' // отправлять куки
})
// На сервере (нельзя использовать * с credentials!)
Access-Control-Allow-Origin: https://myapp.com // конкретный origin, не *
Access-Control-Allow-Credentials: true1. Правильная настройка сервера (лучший вариант)
// Express.js пример
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
if (req.method === 'OPTIONS') {
res.status(204).end()
return
}
next()
})**2. Прокси-сервер** — браузер → твой сервер → чужой API (CORS не применяется к серверным запросам):
Браузер → myapp.com/api/proxy → api.other.com
↑ тот же origin ↑ серверный запрос, SOP не работает**3. JSONP** (устаревший) — только GET, использует <script src> который не подпадает под SOP.
// CORS нельзя обойти из браузера:
// - Изменить заголовки запроса чтобы "притвориться" другим origin
// - Читать ответ без разрешения сервера
// - Использовать fetch с другим Origin заголовком
// Это работает только не в браузере (curl, Node.js, Postman):
// curl -H "Origin: https://evil.com" https://api.example.com/data
// ← сервер ответит, но браузер бы заблокировалCORS — защита **браузера** от атак типа CSRF, где злой сайт пытается сделать запрос к твоему банку от имени твоего браузера.
Access-Control-Allow-Origin — разрешённые origins (* или конкретный)
Access-Control-Allow-Methods — разрешённые HTTP методы
Access-Control-Allow-Headers — разрешённые заголовки запроса
Access-Control-Expose-Headers — заголовки ответа, доступные JS
Access-Control-Allow-Credentials — разрешить куки/авторизацию
Access-Control-Max-Age — время кеширования preflight (секунды)**Начни с SOP**: "CORS существует из-за Same-Origin Policy — браузер блокирует запросы к другому origin в целях безопасности."
**Объясни механизм**: "CORS позволяет серверу указать, каким origins он доверяет, через специальные заголовки ответа."
**Упомяни preflight**: "Для непростых запросов (DELETE, JSON-тело, кастомные заголовки) браузер сначала отправляет OPTIONS preflight-запрос."
**Скажи как решать**: "Правильное решение — настроить CORS-заголовки на сервере. Прокси — альтернатива когда нет доступа к серверу."
**Подчеркни безопасность**: "CORS нельзя обойти из браузера — это защита от CSRF. Серверные запросы SOP не затрагивает."
**Время ответа**: 3-4 минуты.
1. **"CORS — это ошибка которую надо отключить"** или **"просто добавь Access-Control-Allow-Origin: *"** — это игнорирование модели безопасности. Wildcard * нельзя использовать с credentials и это потенциально небезопасно.
2. **"CORS обходится на клиенте"** — нет. CORS — защита браузера. Обойти можно только через прокси на своём сервере, но не из браузерного JS.
3. **Не знать о preflight** — если не понимаешь почему DELETE или POST с JSON вызывает дополнительный OPTIONS-запрос — ты не понимаешь как CORS работает на практике.
Демонстрация простых vs непростых запросов, логика preflight и правильная диагностика CORS-ошибок
// ===== ЧТО ДЕЛАЕТ ЗАПРОС "НЕПРОСТЫМ" (требует preflight) =====
console.log('=== Simple vs Non-Simple Requests ===')
function analyzeRequest(config) {
const { method, headers = {}, body } = config
const simpleMethods = ['GET', 'POST', 'HEAD']
const simpleContentTypes = [
'application/x-www-form-urlencoded',
'multipart/form-data',
'text/plain'
]
const simpleHeaders = [
'accept', 'accept-language', 'content-language', 'content-type'
]
const issues = []
// Проверяем метод
if (!simpleMethods.includes(method.toUpperCase())) {
issues.push(`Метод ${method} не простой (только GET, POST, HEAD)`)
}
// Проверяем Content-Type
const ct = headers['Content-Type'] || headers['content-type'] || ''
if (ct && !simpleContentTypes.some(t => ct.toLowerCase().startsWith(t))) {
issues.push(`Content-Type "${ct}" не простой`)
}
// Проверяем кастомные заголовки
const customHeaders = Object.keys(headers).filter(
h => !simpleHeaders.includes(h.toLowerCase()) && h.toLowerCase() !== 'content-type'
)
if (customHeaders.length > 0) {
issues.push(`Кастомные заголовки: ${customHeaders.join(', ')}`)
}
const needsPreflight = issues.length > 0
return { needsPreflight, reasons: issues }
}
const requests = [
{ label: 'Простой GET', config: { method: 'GET' } },
{ label: 'Простой POST (form)', config: { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } },
{ label: 'POST с JSON', config: { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' } },
{ label: 'POST с Authorization', config: { method: 'POST', headers: { 'Authorization': 'Bearer token' } } },
{ label: 'DELETE запрос', config: { method: 'DELETE' } },
{ label: 'PUT с JSON и Auth', config: { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer token' } } },
]
for (const { label, config } of requests) {
const result = analyzeRequest(config)
const status = result.needsPreflight ? '⚡ PREFLIGHT нужен' : 'OK простой'
console.log(`\n${label}: ${status}`)
if (result.reasons.length > 0) {
result.reasons.forEach(r => console.log(` - ${r}`))
}
}
// ===== СИМУЛЯЦИЯ PREFLIGHT ПОТОКА =====
console.log('\n=== Симуляция Preflight потока ===')
function simulateCorsFlow(clientOrigin, request, serverConfig) {
console.log(`\nКлиент: ${clientOrigin}`)
console.log(`Запрос: ${request.method} ${request.url}`)
const { needsPreflight } = analyzeRequest(request)
if (needsPreflight) {
console.log('\n→ Браузер отправляет OPTIONS preflight:')
console.log(` OPTIONS ${request.url}`)
console.log(` Origin: ${clientOrigin}`)
console.log(` Access-Control-Request-Method: ${request.method}`)
if (request.headers) {
console.log(` Access-Control-Request-Headers: ${Object.keys(request.headers).join(', ')}`)
}
const allowed = serverConfig.allowedOrigins.includes(clientOrigin) ||
serverConfig.allowedOrigins.includes('*')
const methodAllowed = serverConfig.allowedMethods.includes(request.method)
if (allowed && methodAllowed) {
console.log('\n← Сервер отвечает на preflight:')
console.log(` HTTP/1.1 204 No Content`)
console.log(` Access-Control-Allow-Origin: ${clientOrigin}`)
console.log(` Access-Control-Allow-Methods: ${serverConfig.allowedMethods.join(', ')}`)
console.log(` Access-Control-Allow-Headers: ${serverConfig.allowedHeaders.join(', ')}`)
console.log(` Access-Control-Max-Age: 86400`)
console.log('\n→ Preflight прошёл! Отправляем основной запрос...')
console.log('← Основной запрос выполнен успешно')
} else {
console.log(`\n← Сервер отвечает на preflight: заблокировано!`)
if (!allowed) console.log(` Origin ${clientOrigin} не разрешён`)
if (!methodAllowed) console.log(` Метод ${request.method} не разрешён`)
console.log('← Браузер блокирует основной запрос → CORS Error')
}
} else {
console.log('→ Простой запрос — без preflight')
const allowed = serverConfig.allowedOrigins.includes(clientOrigin) ||
serverConfig.allowedOrigins.includes('*')
console.log(allowed
? '← Сервер разрешает: Access-Control-Allow-Origin присутствует'
: '← Сервер не отправил CORS заголовки → браузер блокирует')
}
}
const serverConfig = {
allowedOrigins: ['https://myapp.com', 'https://staging.myapp.com'],
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID']
}
simulateCorsFlow('https://myapp.com', {
method: 'DELETE',
url: 'https://api.backend.com/users/42',
headers: { 'Authorization': 'Bearer token' }
}, serverConfig)
simulateCorsFlow('https://evil.com', {
method: 'POST',
url: 'https://api.backend.com/users',
headers: { 'Content-Type': 'application/json' }
}, serverConfig)Диагностируй CORS-конфигурацию сервера и определи: какие запросы пройдут, какие будут заблокированы. Реализуй функцию checkCors(origin, request, serverHeaders), которая возвращает объект с результатом проверки.
Шаг 2: if (allowedOrigin !== "*" && allowedOrigin !== origin) { result.reason = "Origin не совпадает"; return result }. Шаг 4: если needsPreflight, проверь allowedMethods.includes(method) и все кастомные заголовки в allowedHeaders.
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке