CORS
CORS решает, каким сторонним источникам браузер разрешит обращаться к вашему API.
Winter даёт два уровня: глобальную политику для всего приложения и
пер-маршрутное переопределение атрибутом #[CrossOrigin].
Что такое 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:
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():
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.
Полный конвейер — в разделе «Под капотом» на странице Маршрутизации.
Дальше
- Middleware — своя пред-/постобработка запроса
- Маршрутизация — где живёт
#[CrossOrigin] - Конфигурация — хук
httpCors()вBoot