Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

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. Он ограничивает только скрипты в браузере; доступ закрывает проверка токена.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.