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

Частые ошибки

  1. Access-Control-Allow-Origin: * с credentials — браузер заблокирует, нужен конкретный origin
  2. Нет обработки OPTIONS — сервер возвращает 404 на preflight → все запросы блокируются
  3. CORS на клиенте — CORS настраивается ТОЛЬКО на сервере, клиент ничего не может сделать
  4. Забывают credentials: 'include' — cookies не отправляются, авторизация не работает
  5. Нет Expose-Headers — JS не видит кастомные заголовки ответа (X-Total-Count, X-Request-ID)
  6. Путают CORS и авторизацию — CORS не защищает API, он защищает браузер от нежелательных запросов

Практика

  1. Воспроизвести CORS-ошибку: SPA на 3000, API на 4000 без CORS-заголовков
  2. Настроить cors middleware на Express
  3. Изучить preflight в DevTools → Network: найти OPTIONS запрос
  4. Настроить proxy в Vite/Webpack для dev-окружения
  5. Реализовать credentials: include + конкретный origin

Связанные темы

Ресурсы