CORS
Браузер не разрешает JavaScript читать ответы с другого сайта: скрипт со страницы app.example.com не получит данные от api.example.com, пока сам API этого не разрешит. Разрешение выдают заголовками ответа — этот механизм называется CORS. На запросы из curl, мобильных приложений и других серверов ограничение не действует.
Как выглядит проблема
Если страница другого сайта обратится к /api/posts, запрос дойдёт до Slim и получит ответ, но браузер не отдаст его скрипту, а в консоли появится сообщение о блокировке политикой CORS. Исправляют это на стороне API.
Посредник
// src/Middleware/CorsMiddleware.php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class CorsMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
return $handler->handle($request)
->withHeader('Access-Control-Allow-Origin', 'https://app.example.com');
}
}Посредник добавляет заголовок к готовому ответу. В нём указан адрес сайта, которому разрешено читать ответы, — со схемой, без пути и косой черты в конце.
Привязка
Заголовки нужны только API, поэтому посредник привязывают к его группе:
// config/routes.php
$app->group('/api', function (RouteCollectorProxy $api) {
$api->group('/posts', function (RouteCollectorProxy $group) {
// маршруты статей из прошлых уроков
});
})->add(CorsMiddleware::class);Проверка из терминала:
curl -i http://localhost:8080/api/postsСреди заголовков ответа появится Access-Control-Allow-Origin.
Предварительный запрос
Перед запросом с методом POST, PUT или DELETE, с JSON или с заголовком Authorization браузер сначала сам отправляет запрос методом OPTIONS — спрашивает, что разрешено. Ему нужно ответить списком методов и заголовков. Дополните посредник:
// src/Middleware/CorsMiddleware.php
return $handler->handle($request)
->withHeader('Access-Control-Allow-Origin', 'https://app.example.com')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');Посредник группы выполняется только тогда, когда для запроса нашёлся маршрут. Маршрутов для OPTIONS у API нет, поэтому добавьте в группу /api один общий:
// config/routes.php
$api->options('/{routes:.+}', function (Request $request, Response $response) {
return $response;
});Шаблон .+ подходит под любой адрес внутри группы. Обработчик возвращает пустой ответ, а заголовки к нему добавляет CorsMiddleware.
Заголовки CORS
| Заголовок | Что разрешает |
|---|---|
Access-Control-Allow-Origin | С какого сайта можно читать ответы |
Access-Control-Allow-Methods | Какими методами можно обращаться |
Access-Control-Allow-Headers | Какие заголовки запроса можно присылать |
Access-Control-Max-Age | На сколько секунд браузер запомнит разрешение |
Несколько сайтов
В заголовке может стоять только один адрес. Если разрешённых сайтов несколько, список хранят в настройках, а посредник сравнивает с ним заголовок запроса Origin и подставляет в ответ совпавший адрес.
Частая ошибка
Значение * в Access-Control-Allow-Origin разрешает чтение любому сайту. Для открытого API только на чтение это допустимо, но если API закрыт токеном, перечисляйте конкретные адреса. И помните: CORS — не защита API. Он ограничивает только скрипты в браузере; доступ закрывает проверка токена.