Настройка CORS

CORS (Cross-Origin Resource Sharing) — механизм браузерной безопасности, регулирующий доступ к ресурсам между разными источниками (origin). Источник включает:

  • протокол;
  • домен;
  • порт.

Если frontend-приложение на Vite работает по адресу:

http://localhost:5173

а API расположен по адресу:

http://localhost:3000

браузер считает их разными источниками из-за различия портов. Любой запрос между ними попадает под действие политики CORS.

При отсутствии корректных CORS-заголовков браузер блокирует запросы независимо от того, успешно ли сервер обработал их на своей стороне.

Vite активно используется вместе с backend API, поэтому настройка CORS становится одной из наиболее частых задач при разработке.


Как Vite взаимодействует с CORS

Vite сам по себе не снимает ограничения браузера. Он лишь:

  • запускает dev-сервер;
  • проксирует запросы;
  • добавляет собственные HTTP-заголовки;
  • управляет dev-средой.

Проблема CORS решается:

  • либо на backend-сервере;
  • либо через proxy-конфигурацию Vite;
  • либо через специальные middleware.

Опция server.cors

Vite предоставляет встроенную настройку:

export default {
    server: {
        cors: true
    }
}

При включении этой опции dev-сервер добавляет CORS-заголовки к собственным ресурсам:

Access-Control-Allow-Origin: *

Это влияет на:

  • HMR;
  • JS-модули;
  • CSS;
  • ассеты;
  • dev-runtime Vite.

Однако данная настройка не исправляет CORS между frontend и внешним API.


Базовая настройка CORS

Пример:

import { defineConfig } from 'vite'

export default defineConfig({
    server: {
        cors: true
    }
})

Эквивалентная форма:

server: {
    cors: {}
}

Vite использует middleware библиотеки cors под капотом.


Расширенная конфигурация

server.cors может принимать объект настроек:

import { defineConfig } from 'vite'

export default defineConfig({
    server: {
        cors: {
            origin: 'http://localhost:3000',
            methods: ['GET', 'POST'],
            allowedHeaders: ['Content-Type'],
            credentials: true
        }
    }
})

Настройка origin

Разрешённые источники:

cors: {
    origin: 'http://localhost:3000'
}

Массив:

cors: {
    origin: [
        'http://localhost:3000',
        'http://localhost:8080'
    ]
}

Регулярное выражение:

cors: {
    origin: /localhost/
}

Функция:

cors: {
    origin(origin, callback) {
        if (!origin) {
            callback(null, true)
            return
        }

        if (origin.includes('localhost')) {
            callback(null, true)
        } else {
            callback(new Error('Not allowed'))
        }
    }
}

Функциональная форма полезна при сложной политике безопасности.


Настройка methods

Определяет допустимые HTTP-методы:

cors: {
    methods: ['GET', 'POST', 'PUT', 'DELETE']
}

Если метод отсутствует в списке, браузер заблокирует запрос после preflight-проверки.


Настройка allowedHeaders

Указывает разрешённые заголовки:

cors: {
    allowedHeaders: [
        'Content-Type',
        'Authorization'
    ]
}

Часто требуется для JWT-аутентификации:

Authorization: Bearer token

Настройка credentials

Разрешает передачу:

  • cookies;
  • HTTP-auth;
  • TLS client certificates.

Пример:

cors: {
    credentials: true
}

В этом случае нельзя использовать:

Access-Control-Allow-Origin: *

Необходимо явно указать origin:

cors: {
    origin: 'http://localhost:5173',
    credentials: true
}

Настройка exposedHeaders

По умолчанию браузер скрывает часть заголовков ответа.

Разрешение:

cors: {
    exposedHeaders: [
        'X-Total-Count',
        'X-Request-Id'
    ]
}

После этого frontend сможет читать:

response.headers.get('X-Total-Count')

Настройка maxAge

Определяет время кеширования preflight-запросов:

cors: {
    maxAge: 86400
}

Это уменьшает количество OPTIONS-запросов.


Preflight-запросы

Перед некоторыми запросами браузер автоматически отправляет:

OPTIONS

Это называется preflight request.

Он используется при:

  • нестандартных заголовках;
  • методах PUT/PATCH/DELETE;
  • Content-Type: application/json;
  • credentials.

Пример:

OPTIONS /api/users

Сервер должен вернуть:

Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers

Иначе основной запрос не будет отправлен.


Простые запросы

Preflight не выполняется для:

  • GET;
  • HEAD;
  • POST с application/x-www-form-urlencoded;
  • POST с multipart/form-data;
  • POST с text/plain.

Такие запросы называются simple requests.


Почему server.cors не решает проблему API

Частая ошибка:

server: {
    cors: true
}

и ожидание, что запросы к backend заработают автоматически.

Но запрос:

fetch('http://localhost:3000/api')

обрабатывается backend-сервером, а не Vite.

Следовательно:

  • именно backend обязан отдавать CORS-заголовки;
  • либо запрос должен проходить через proxy.

Использование proxy вместо CORS

Наиболее популярный подход при разработке — proxy.

Пример:

import { defineConfig } from 'vite'

export default defineConfig({
    server: {
        proxy: {
            '/api': {
                target: 'http://localhost:3000',
                changeOrigin: true
            }
        }
    }
})

Frontend:

fetch('/api/users')

Vite:

  1. принимает запрос;
  2. перенаправляет его на backend;
  3. возвращает ответ браузеру.

Для браузера запрос остаётся same-origin.

CORS полностью обходится.


Преимущества proxy

Отсутствие CORS-ошибок

Браузер работает только с origin Vite.

Удобство локальной разработки

Не требуется изменять backend.

Сокрытие внутренних адресов API

Frontend не знает реальный backend URL.

Единая точка входа

Все запросы идут через Vite.


Настройка changeOrigin

proxy: {
    '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true
    }
}

Изменяет заголовок:

Host

Это важно для:

  • nginx;
  • virtual hosts;
  • cloud backend;
  • API gateway.

Настройка rewrite

Позволяет изменять путь запроса.

Пример:

proxy: {
    '/api': {
        target: 'http://localhost:3000',
        rewrite: path => path.replace(/^\/api/, '')
    }
}

Запрос:

/api/users

превратится в:

/users

Настройка HTTPS proxy

proxy: {
    '/api': {
        target: 'https://localhost:8443',
        secure: false
    }
}

secure: false отключает проверку SSL-сертификата.

Используется для self-signed сертификатов в dev-среде.


WebSocket и CORS

Vite активно использует WebSocket для HMR.

Иногда reverse proxy или backend блокируют WS-соединения.

Настройка:

proxy: {
    '/socket': {
        target: 'ws://localhost:3000',
        ws: true
    }
}

Настройка backend вместо Vite

Правильное место настройки CORS — backend.

Express

import cors from 'cors'
import express from 'express'

const app = express()

app.use(cors({
    origin: 'http://localhost:5173',
    credentials: true
}))

Fastify

import Fastify from 'fastify'
import cors from '@fastify/cors'

const app = Fastify()

await app.register(cors, {
    origin: 'http://localhost:5173'
})

NestJS

const app = await NestFactory.create(AppModule)

app.enableCors({
    origin: 'http://localhost:5173'
})

Laravel

return [

    'paths' => ['api/*'],

    'allowed_methods' => ['*'],

    'allowed_origins' => ['http://localhost:5173'],

];

Spring Boot

@Configuration
public class CorsConfig {

    @Bean
    public WebMvcConfigurer corsConfigurer() {

        return new WebMvcConfigurer() {

            @Override
            public void addCorsMappings(CorsRegistry registry) {

                registry.addMapping("/api/**")
                    .allowedOrigins("http://localhost:5173");
            }
        };
    }
}

Ошибки при работе с CORS

No 'Access-Control-Allow-Origin' header

Наиболее распространённая ошибка.

Причины:

  • backend не возвращает CORS-заголовки;
  • proxy не настроен;
  • origin запрещён.

Credentials flag is true

Ошибка:

The value of the 'Access-Control-Allow-Origin' header
must not be '*'

Возникает при:

credentials: true

и одновременно:

Access-Control-Allow-Origin: *

Ошибка preflight

Причины:

  • OPTIONS не поддерживается сервером;
  • отсутствуют Allow-Headers;
  • отсутствуют Allow-Methods.

Mixed Content

Если frontend работает через HTTPS:

https://localhost:5173

а API через HTTP:

http://localhost:3000

браузер блокирует запрос независимо от CORS.


Проверка CORS через DevTools

Вкладка:

Network

Позволяет анализировать:

  • OPTIONS-запросы;
  • response headers;
  • request headers;
  • origin;
  • статус preflight.

Особое внимание:

Access-Control-Allow-Origin
Access-Control-Allow-Credentials
Access-Control-Allow-Headers
Access-Control-Allow-Methods

Использование cookies с Vite

Frontend:

fetch('/api/profile', {
    credentials: 'include'
})

Backend:

Access-Control-Allow-Credentials: true

и:

Access-Control-Allow-Origin: http://localhost:5173

Cookie также должна иметь:

SameSite=None
Secure

для cross-site запросов.


CORS и SSR

При SSR часть запросов выполняется на сервере Node.js.

В этом случае ограничения браузера отсутствуют.

Пример:

const response = await fetch('http://api.internal/users')

Node.js не применяет browser CORS policy.

Однако после гидратации браузерные ограничения снова начинают действовать.


Разница между CORS и CSRF

CORS:

  • механизм браузерной безопасности;
  • регулирует междоменные запросы.

CSRF:

  • атака на доверенные запросы;
  • связана с cookies и авторизацией.

CORS не защищает от CSRF.


Production-среда

В production Vite обычно:

  • собирает frontend;
  • не работает как сервер приложений.

Следовательно:

  • server.cors больше не используется;
  • CORS настраивается в nginx, backend или CDN.

Пример production-конфигурации nginx

location /api {

    add_header Access-Control-Allow-Origin https://example.com;
    add_header Access-Control-Allow-Credentials true;

    proxy_pass http://backend;
}

Безопасность CORS

Плохая практика:

Access-Control-Allow-Origin: *

для приватного API.

Особенно опасно вместе с:

Access-Control-Allow-Credentials: true

Безопаснее:

  • явно перечислять origin;
  • ограничивать методы;
  • ограничивать заголовки;
  • отключать credentials без необходимости.

Полезная схема разработки

Frontend

http://localhost:5173

Backend

http://localhost:3000

Конфигурация Vite

server: {
    proxy: {
        '/api': {
            target: 'http://localhost:3000',
            changeOrigin: true
        }
    }
}

Запросы frontend

fetch('/api/users')

Результат

  • отсутствуют CORS-ошибки;
  • backend не требует dev-настройки CORS;
  • одинаковый API-путь в dev и production.