CORS
Зачем нужно
CORS (Cross-Origin Resource Sharing) — механизм, позволяющий серверу указать, с каких доменов разрешены запросы. По умолчанию браузер блокирует запросы к чужим доменам (Same-Origin Policy). CORS — способ «разрешить» кросс-доменные запросы через HTTP-заголовки.
Где используется
- SPA на
localhost:3000обращается к API наlocalhost:4000 - Фронтенд на
app.example.comиспользует API наapi.example.com - Работа со сторонними API (Google Maps, GitHub API)
- CDN-ресурсы (шрифты, скрипты)
Same-Origin Policy
Браузер считает два URL «одним источником» только если совпадают протокол + хост + порт:
https://example.com/page — origin: https://example.com
Тот же origin:
https://example.com/other ✅ (другой путь — OK)
https://example.com:443/x ✅ (443 = default для https)
Другой origin:
http://example.com ❌ (другой протокол)
https://api.example.com ❌ (другой хост)
https://example.com:8080 ❌ (другой порт)
// SPA на http://localhost:3000 делает запрос:
fetch('http://localhost:4000/api/users')
.then(res => res.json())
.catch(err => console.error(err));
// Браузер ЗАБЛОКИРУЕТ и покажет ошибку:
// Access to fetch at 'http://localhost:4000/api/users'
// from origin 'http://localhost:3000' has been blocked by CORS policy:
// No 'Access-Control-Allow-Origin' header is present
Как работает CORS
Простые запросы (Simple Requests)
GET, HEAD, POST с простыми заголовками — запрос отправляется сразу:
Браузер → Сервер:
GET /api/users
Origin: http://localhost:3000 ← Браузер автоматически добавляет
Сервер → Браузер:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000 ← Разрешение
Content-Type: application/json
[{"id": 1, "name": "Антон"}]
Браузер проверяет Access-Control-Allow-Origin:
✅ Совпадает → данные доступны JavaScript
❌ Не совпадает → блокирует ответ
Preflight-запросы (предварительные)
Для «сложных» запросов (PUT, DELETE, кастомные заголовки) браузер сначала отправляет OPTIONS:
1. Браузер отправляет OPTIONS (preflight):
OPTIONS /api/users/42
Origin: http://localhost:3000
Access-Control-Request-Method: DELETE ← Хочу DELETE
Access-Control-Request-Headers: Authorization ← С этим заголовком
2. Сервер отвечает что разрешено:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400 ← Кэшировать preflight на 24ч
3. Браузер проверяет — всё OK, отправляет основной запрос:
DELETE /api/users/42
Origin: http://localhost:3000
Authorization: Bearer eyJ...
4. Сервер отвечает:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
CORS-заголовки
Заголовки ответа сервера
Access-Control-Allow-Origin: https://example.com
— Какой origin разрешён (* = любой)
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
— Разрешённые HTTP-методы
Access-Control-Allow-Headers: Content-Type, Authorization
— Разрешённые заголовки запроса
Access-Control-Allow-Credentials: true
— Разрешить отправку cookies
Access-Control-Expose-Headers: X-Total-Count
— Какие заголовки ответа доступны JS (по умолчанию только стандартные)
Access-Control-Max-Age: 86400
— Сколько секунд кэшировать preflight-ответ
Настройка CORS на сервере
Express.js (middleware cors)
npm install cors
const express = require('express');
const cors = require('cors');
const app = express;
// Вариант 1: Разрешить всё (для разработки)
app.use(cors);
// Вариант 2: Конкретные настройки
app.use(cors({
origin: 'http://localhost:3000', // Один origin
// origin: ['http://localhost:3000', 'https://app.example.com'], // Несколько
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // Разрешить cookies
maxAge: 86400, // Кэш preflight 24ч
}));
// Вариант 3: Динамический origin
app.use(cors({
origin: (origin, callback) => {
const whitelist = [
'http://localhost:3000',
'https://app.example.com',
];
if (!origin || whitelist.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
}));
Ручная настройка (без библиотеки)
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
// Ответ на preflight
if (req.method === 'OPTIONS') {
res.header('Access-Control-Max-Age', '86400');
return res.status(204).send;
}
next;
});
Credentials (Cookies)
// Клиент — нужно явно указать credentials
fetch('http://api.example.com/profile', {
credentials: 'include', // Отправить cookies
});
// Сервер — обязательные заголовки:
// Access-Control-Allow-Credentials: true
// Access-Control-Allow-Origin: https://example.com ← НЕ * !!!
Важно: при credentials: 'include' нельзя использовать Access-Control-Allow-Origin: * — нужен конкретный origin.
Proxy как обход CORS (dev)
В разработке часто удобнее настроить прокси вместо CORS:
// Vite — vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:4000',
changeOrigin: true,
},
},
},
};
// Webpack — devServer
module.exports = {
devServer: {
proxy: {
'/api': 'http://localhost:4000',
},
},
};
// Теперь запрос идёт на тот же origin (нет CORS):
fetch('/api/users');
// Dev server проксирует → http://localhost:4000/api/users
Частые ошибки
Access-Control-Allow-Origin: *с credentials — браузер заблокирует, нужен конкретный origin- Нет обработки OPTIONS — сервер возвращает 404 на preflight → все запросы блокируются
- CORS на клиенте — CORS настраивается ТОЛЬКО на сервере, клиент ничего не может сделать
- Забывают
credentials: 'include'— cookies не отправляются, авторизация не работает - Нет
Expose-Headers— JS не видит кастомные заголовки ответа (X-Total-Count, X-Request-ID) - Путают CORS и авторизацию — CORS не защищает API, он защищает браузер от нежелательных запросов
Практика
- Воспроизвести CORS-ошибку: SPA на 3000, API на 4000 без CORS-заголовков
- Настроить cors middleware на Express
- Изучить preflight в DevTools → Network: найти OPTIONS запрос
- Настроить proxy в Vite/Webpack для dev-окружения
- Реализовать credentials: include + конкретный origin
Связанные темы
- HTTP протокол — заголовки и методы
- REST API — CORS при работе с REST
- Cookies и сессии — cookies в кросс-доменных запросах
- Клиент-серверное взаимодействие — архитектура запросов