2 · курс
0%
Главная/🔌REST API

39REST API — форматы, аутентификация, версии

ContentNegotiator и Serializer (JSON/XML, envelope, pretty print); аутентификация HttpBasicAuth/HttpBearerAuth/QueryParamAuth/CompositeAuth и findIdentityByAccessToken; ограничение частоты RateLimitInterface; версионирование модулями v1/v2; обработка ошибок и формат ответа; фильтрация коллекций DataFilter.

⏱ ~6 мин 📚 6 глав руководства

После быстрого старта остаются вопросы взрослого API: как отвечать в нужном формате, как узнавать клиента без сессии, как не дать одному клиенту положить сервер, как выпускать v2, не ломая v1, и как отдавать ошибки единообразно. Всё это — фильтры и настройки yii\rest.

Формат ответа#

Два шага: ContentNegotiator выбирает формат по Accept/GET-параметру, а Response с нужным formatter сериализует данные из $response->data.

PHP
// yii\rest\Controller уже включает contentNegotiator; настройка:
public function behaviors()
{
    $behaviors = parent::behaviors();
    $behaviors['contentNegotiator']['formats'] = [
        'application/json' => Response::FORMAT_JSON,
        'application/xml' => Response::FORMAT_XML,
    ];
    $behaviors['contentNegotiator']['formatParam'] = '_format';   // ?_format=xml
    return $behaviors;
}

Поддерживаются Accept: application/json; q=1.0, */*; q=0.1. Форматы ответа настраиваются в response.formatters:

PHP
'response' => [
    'formatters' => [
        'json' => [
            'class' => 'yii\web\JsonResponseFormatter',
            'prettyPrint' => YII_DEBUG,
            'encodeOptions' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
        ],
        'xml' => ['class' => 'yii\web\XmlResponseFormatter', 'rootTag' => 'response', 'itemTag' => 'item'],
    ],
],

Сериализация данных#

yii\rest\Serializer превращает Arrayable (модели) и DataProviderInterface (коллекции) в массивы с учётом fields/expand:

PHP
public $serializer = [
    'class' => 'yii\rest\Serializer',
    'collectionEnvelope' => 'items',   // коллекция внутри {"items": …, "_links": …, "_meta": …}
    'fieldsParam' => 'fields', 'expandParam' => 'expand',
    'totalCountHeader' => 'X-Pagination-Total-Count', …
];

Свои объекты — реализуйте Arrayable или отдавайте массивы.

Аутентификация#

API обычно без сессий: клиент присылает токен в каждом запросе.

config/web.phpPHP
'user' => [
    'identityClass' => 'app\models\User',
    'enableAutoLogin' => false,
    'enableSession' => false,           // не создавать сессии для API
    'loginUrl' => null,                 // не редиректить — отдавать 401
],
controllers/UserController.phpPHP
use yii\filters\auth\HttpBearerAuth;

public function behaviors()
{
    $behaviors = parent::behaviors();
    $behaviors['authenticator'] = [
        'class' => HttpBearerAuth::class,
        // 'except' => ['login', 'options'],   // OPTIONS должен быть доступен для CORS
    ];
    return $behaviors;
}
models/User.phpPHP
public static function findIdentityByAccessToken($token, $type = null)
{
    return static::findOne(['access_token' => $token, 'status' => self::STATUS_ACTIVE]);
}
HttpBasicAuth
логин/пароль в заголовке Authorization: Basic; по умолчанию логин = токен, но можно auth — колбэк проверки пароля
HttpBearerAuth
Authorization: Bearer <token> — стандарт для API
HttpHeaderAuth
токен в произвольном заголовке (header, pattern), 2.0.14
QueryParamAuth
?access-token=… — только когда заголовки недоступны (утекает в логи)
CompositeAuth
попробовать несколько (authMethods) по очереди
PHP
$behaviors['authenticator'] = [
    'class' => CompositeAuth::class,
    'authMethods' => [HttpBasicAuth::class, HttpBearerAuth::class, QueryParamAuth::class],
];

Неудача — 401 Unauthorized. Аутентификатор вызывает Yii::$app->user->loginByAccessToken($token, $type) → findIdentityByAccessToken(); $type — класс аутентификатора, если токены разных видов.

Ограничение частоты (rate limiting)#

models/User.phpPHP
use yii\filters\RateLimitInterface;

class User extends ActiveRecord implements IdentityInterface, RateLimitInterface
{
    public function getRateLimit($request, $action)
    {
        return [100, 600];        // 100 запросов за 600 секунд
    }

    public function loadAllowance($request, $action)
    {
        return [$this->allowance, $this->allowance_updated_at];   // столбцы в таблице
    }

    public function saveAllowance($request, $action, $allowance, $timestamp)
    {
        $this->allowance = $allowance;
        $this->allowance_updated_at = $timestamp;
        $this->save(false);
    }
}

RateLimiter уже подключён в yii\rest\Controller и включается автоматически, если identity реализует интерфейс. Ответ при превышении — 429 Too Many Requests; в заголовках X-Rate-Limit-Limit, X-Rate-Limit-Remaining, X-Rate-Limit-Reset. Алгоритм — leaky bucket. Хранить allowance можно в кэше вместо БД.

Версионирование#

Версия в URL — самый прозрачный вариант: каждая версия — модуль.

api/
    modules/
        v1/
            controllers/UserController.php
            models/User.php
            Module.php
        v2/
            ...
PHP
'modules' => [
    'v1' => ['class' => 'app\modules\v1\Module'],
    'v2' => ['class' => 'app\modules\v2\Module'],
],
'urlManager' => [
    'rules' => [
        ['class' => 'yii\rest\UrlRule', 'controller' => ['v1/user', 'v1/post']],
        ['class' => 'yii\rest\UrlRule', 'controller' => ['v2/user', 'v2/post']],
    ],
],
// → /v1/users, /v2/users

Общие модели — в common, версионные различия — в fields()/контроллерах модулей. Минорные изменения (новое поле, новый параметр) — совместимы, версия та же; мажорные — новая версия. Альтернатива — версия в заголовке Accept: application/json; version=v2 и разбор в ContentNegotiator/фильтре, но URL проще для клиентов и кэшей.

Обработка ошибок#

Исключения превращаются в ответы автоматически:

Ситуация Код
Успех GET/PUT/PATCH200
POST создал ресурс201
DELETE успешно204
Неверный запрос (JSON не разобран)400
Нет аутентификации401
Аутентифицирован, но нельзя (ForbiddenHttpException)403
Не найдено (NotFoundHttpException)404
Метод не поддерживается (MethodNotAllowedHttpException)405
Формат не поддерживается415
Ошибки валидации (save() вернул false)422
Превышен лимит429
Внутренняя ошибка500

Тело ошибки: {"name": "Not Found", "message": "…", "code": 0, "status": 404} (в debug — плюс type, file, line, стек). Свой формат — событием beforeSend ответа:

PHP
'response' => [
    'class' => 'yii\web\Response',
    'on beforeSend' => function ($event) {
        $response = $event->sender;
        if ($response->data !== null && Yii::$app->request->get('suppress_response_code')) {
            $response->data = ['success' => $response->isSuccessful, 'data' => $response->data];
            $response->statusCode = 200;
        }
    },
],

Фильтрация коллекций#

С 2.0.13 у IndexAction есть dataFilter — безопасный разбор условий из запроса:

PHP
public function actions()
{
    $actions = parent::actions();
    $actions['index']['dataFilter'] = [
        'class' => 'yii\data\ActiveDataFilter',
        'searchModel' => 'app\models\PostSearch',   // модель с rules(): какие атрибуты и какие типы допустимы
    ];
    return $actions;
}
GET /posts?filter[status]=1&filter[title][like]=yii&filter[id][in][]=1&filter[id][in][]=2
GET /posts?filter[or][0][author_id]=1&filter[or][1][created_at][gt]=1600000000

Операторы: and, or, not, lt, gt, lte, gte, eq, neq, in, nin, like. Модель поиска не обязана быть AR — достаточно Model с rules(). Без searchModel — DataFilter с настройкой attributeMap/operatorTypes. Условие превращается в where провайдера; недопустимое — 422.

Вручную то же самое: prepareDataProvider + andFilterWhere по Yii::$app->request->get().

Проверь себя5

Фильтр ContentNegotiator по заголовку Accept (или параметру formatParam) — если формат в списке formats.

findIdentityByAccessToken($token, $type) через Yii::$app->user->loginByAccessToken().

Реализовать RateLimitInterface (getRateLimit, loadAllowance, saveAllowance) в identity-классе — RateLimiter уже подключён.

422 с массивом [{"field": "…", "message": "…"}].

Модуль на версию (v1, v2) с собственными контроллерами и правилами yii\rest\UrlRule с префиксом модуля.