Основы веб-разработки

CORS

CORS решает, каким сторонним источникам браузер разрешит обращаться к вашему API. Winter даёт два уровня: глобальную политику для всего приложения и пер-маршрутное переопределение атрибутом #[CrossOrigin].

Глобально Cors::configure()На маршруте #[CrossOrigin]Preflight автоматически

Что такое CORS и зачем

CORS — Cross-Origin Resource Sharing, «совместное использование ресурсов между источниками». Это механизм, которым сервер разрешает браузеру отдавать ответ коду с другого источника.

Источник (origin) — это тройка «схема + хост + порт»: https://app.example.com и https://api.example.comразные источники. По умолчанию браузеры действуют по правилу same-origin policy: JavaScript со страницы одного источника не может прочитать ответ от другого. Это защита от того, чтобы чужой сайт дёргал ваш API от имени пользователя.

Проблема. Типичная связка «SPA + API» живёт на разных источниках: фронтенд на app.example.com, API на api.example.com. Браузер по умолчанию заблокирует запрос фронтенда к API — в консоли появится знакомое CORS policy: No 'Access-Control-Allow-Origin' header.

Решение. Сервер должен явно сказать браузеру, каким источникам он доверяет — через заголовки ответа (Access-Control-Allow-Origin и родственные). Winter проставляет эти заголовки за вас: глобально для всего приложения и точечно на конкретном маршруте. Об этом и раздел.

CORS — это про браузер

CORS проверяет браузер, а не сервер. Запросы из curl, Postman или бэкенд-к-бэкенду ограничение same-origin не касается — заголовки CORS на них не влияют.

Глобальный CORS

Настраивается один раз в хуке httpCors() класса Boot. Применяется ко всем ответам — включая 404 и 500:

bootstrap.php
use Flytachi\Winter\K2\Http\Cors;

protected static function httpCors(): void
{
  Cors::configure(
      origins:       ['https://app.example.com'],
      allowHeaders:  ['Authorization', 'Content-Type'],
      exposeHeaders: ['X-Request-Id'],
      credentials:   true,
      maxAge:        3600,
  );
}

Без вызова Cors::configure() кросс-доменная политика не добавляется.

Пер-маршрутный CORS

#[CrossOrigin] на контроллере или методе переопределяет (не дополняет) глобальную политику для этого маршрута. Набор параметров тот же, что у Cors::configure():

php
use Flytachi\Winter\K2\Route\Annotation\CrossOrigin;

// На весь контроллер
#[CrossOrigin(origins: ['https://admin.example.com'], credentials: true)]
class AdminController extends Controller { /* ... */ }

// На отдельный метод
#[CrossOrigin(origins: ['https://partner.example.com'], maxAge: 3600)]
#[GetMapping('feed')]
public function feed(): ResponseEntity { /* ... */ }

Переопределение, а не слияние

#[CrossOrigin] полностью заменяет глобальную конфигурацию для своего маршрута. Указывайте в нём все нужные параметры — они не наследуются от Cors::configure().

Параметры

Одинаковы для Cors::configure() и #[CrossOrigin]:

Параметр По умолчанию Поведение
origins [] Разрешённые источники. Пусто → Access-Control-Allow-Origin: *. Несколько → отражает совпавший Origin и добавляет Vary: Origin
allowHeaders [] Разрешённые заголовки запроса. Пусто → отражает Access-Control-Request-Headers
exposeHeaders [] Заголовки ответа, видимые из JavaScript
credentials false Отправляет Access-Control-Allow-Credentials: true. Требует явных origins (несовместимо с *)
maxAge 0 TTL кеша preflight (Access-Control-Max-Age). 0 — заголовок не отправляется
vary [] Дополнительные значения заголовка Vary

credentials и звёздочка

credentials: true несовместимо с origins: [] (то есть с *). Браузер отклонит такую комбинацию — при работе с куками/авторизацией всегда указывайте явные источники.

Preflight-запросы

Предварительный запрос OPTIONS фреймворк обрабатывает сам: отвечает 204 с нужными CORS-заголовками до вызова обработчика. Отдельный метод под OPTIONS писать не нужно.

Во время preflight роутер определяет, какой маршрут будет вызван реальным методом (из заголовка Access-Control-Request-Method), и берёт его #[CrossOrigin], если он задан.


Под капотом

Как и когда пишутся CORS-заголовки. Для повседневной работы знать не обязательно.

  • Глобальные заголовки — до диспетчеризации. Это шаг 5 конвейера Router::handle(), до поиска маршрута. Поэтому CORS присутствует и на ответах 404, 405, 500 — браузер видит понятную ошибку, а не «CORS blocked».
  • Пер-маршрутный CORS — после сопоставления. Это шаг 8: найдя маршрут с #[CrossOrigin], роутер заменяет им глобальные заголовки.
  • Preflight зондирует цель. На OPTIONS (шаг 6) роутер пробует диспетчер с предполагаемым методом браузера, чтобы найти #[CrossOrigin] нужного маршрута ещё до ответа 204.

Полный конвейер — в разделе «Под капотом» на странице Маршрутизации.

Дальше