Как стать автором
Обновить

Запросы к апи с бэка с повтором если был ответ 401 (UnAuthorized) на примере Mercuryo. PHP, Yii2

Время на прочтение6 мин
Количество просмотров2.2K

Когда работаешь в проекте со сторонними апи предоставляющими какой-либо сервис, то необходимо делать к ним запросы с бэкенда и как по мне, делать это с бекэнда бывает не так удобно как с фронтенда. Тем более если нужное апи авторизует запросы по временному токену, который действует только какое-то время (обычно 24 ч.) и потом становится не действительным. В данной статье будет рассмотрен способ автоматического обновления такого токена непосредственно в процессе запроса ресурса удалённого сервиса.

Существует несколько способов аутентификации http запросов. Один из них использование постоянного api-key в заголовке или в query части запроса, который добавляется ко всем запросам требующим авторизации. Так (по заголовоку) например работает api яндекс такси. Другой тип аутентификации, это также использование заголовка Authorization с токеном, который может быть временным и для получения которого используется другой метод авторизации по логин-паролью. Так например, работали ситимобил и гетт. В случае когда такой токен (временный) истекает, то необходимо вновь пройти авторизацию через логин-пароль. Сам токен в свою очередь может быть выпущен в виде jwt (json web token). В целом первый способ называется авторизация по api key, а вторая bearer авторизацией.

В данной статье для примера будем использовать апи сервиса mercuryo. У данного сервиса апи реализовано по видоизменённой схеме с комбинацией api-key и временным jwt в качестве его значения. Для первичной аутентификаци используется запрос к методу sign-in по стандартной api-key схеме с использованием постоянного Sdk-Partner-Token. В ответе приходит временный jwt с помощью, которого уже и делаются запросы к ендпроинтам апи. Кроме того, есть возможность обновить валидный (не истёкший) jwt с помощью метода /refresh-token. В данном случае идёт речь об партнёрском апи, через которое партнёр может подключать своих пользователей к сервису и управлять их действиями из своего интерфейса. Соответственно все методы апи применяются к этому пользователю, а не к партнёру как таковому. В том числе и sign-inи остальные. Идентификация пользователей происходит по jwt. А при первоначальном логине используется почта (или телефон/ууид юзера).

Для работы с истекающими токенами можно хранить время получения токена и время его действия. Тогда при запросе можно проверить действителен ли ещё токен и если нет, то пройти повторную аутентификацию. В данной статье мы рассмотрим другой способ, который основан на использовании клиента, который в случае ответа 401 UnAuthorized делает запрос на повторную аутенфикацию и делает повторный запрос с новым токеном без участия со стороны пользователя таким клиентом..

В одной из наших предыдущих статей мы использовали общий класс отправитель различных сообщений Sender. Он ничего из себя не представлял, кроме одного метода с вызовом в нём метода send на http клиенте. Взяв его за основу добавим в него функциональность необходимую для получения желаемого от него поведения. Для этого используем код на основе генераторов добавим ему слой middleware.

<?php

namespace app\services\backend\email;

use app\interfaces\MessageInterface;
use app\interfaces\SenderInterface;
use app\models\Email\RestMessage;
use app\services\backend\infrastructure\ClientInterface;
use Generator;
use yii\base\InvalidConfigException;
use yii\httpclient\Client;
use yii\httpclient\Exception;
use yii\httpclient\Response;

class Sender implements SenderInterface
{
    private array $middlewares;
    private int   $currentMiddleware = 0;
    private RestMessage $message;
    /** @var Client $client */
    private $client;
    private Response $response;

    public function __construct(ClientInterface $client)
    {

        $this->client = $client;
    }

    public function send(MessageInterface $message)
    {
        /** @var RestMessage $message */
        $this->message = $message;
        /** @var Generator $gen */
        $gen = $this->trySend($message);
        foreach ($gen as $n => $s) {
            $this->next();
        }
        /** @var Response $res */
        $res = $gen->getReturn();
        return $res;
    }

    /**
     * @throws Exception
     * @throws InvalidConfigException
     */
    public function trySend(MessageInterface $message)
    {
        $statusCode = null;
        $n = 0;
        while ($statusCode != 200 && $n < 2) {
            $res = $this->client->send($this->message);
            $this->response = $res;
            $statusCode = $res->getStatusCode();
            $n++;
            if ($statusCode != 200){
                yield $res;
            }
        }
        return $res;
    }

    public function middleware(array $middlewares): self
    {
        foreach ($middlewares as $closure) {
            $this->middlewares[] = $closure;
        }
        return $this;
    }

    public function next()
    {
        $current = $this->currentMiddleware++;
        if (isset($this->middlewares[$current])) {
            $do = $this->middlewares[$current]($this->message, $this->response, [$this, 'next']);
            if (!$do) {
                $this->currentMiddleware = 0;
            }
        } else {
            $this->currentMiddleware = 0;
        }
    }
}

Теперь в методе Sender::send() создаётся генератор на основе которого в цикле foreach происходят вызовы метода Sender::trySend() через итератор и вызовы middleware через Sender::next() в теле цикла. В методе Sender::trySend() делаются запросы через клиент пока не будет получен ответ со статусом 200 или n < 2. Если статус ответа не 200, то возвращается новый элемент для внешнего цикла foreach в котором выполняется запуск выполнения middleware. Соответственно, после выполнения всех middleware происходит новый запрос через клиента.

Теперь рассмотрим применение middleware для повторного запроса к апи с обновлённым токеном в MercuryoClient::class.

<?php

namespace app\services\backend\finance\crypto;

use app\interfaces\finance\crypto\CryptoClientInterface;
use app\interfaces\SenderInterface;
use app\models\Email\RestMessage;
use app\models\User\User;
use app\services\backend\email\Sender;
use app\services\backend\infrastructure\ClientInterface;
use app\services\backend\infrastructure\RestClient;

class MercuryoClient implements CryptoClientInterface
{
    private Sender     $sender;
    private RestClient $client;
    private string     $ua;
    private string     $token;

    public function __construct(
        SenderInterface $sender,
        ClientInterface $client,
        string          $token,
        string          $userAgent
    )
    {
        $this->sender = $sender;
        $this->client = $client;
        $this->token  = $token;
        $this->ua     = $userAgent;
    }

    /**
     * Register user in the mercuryo
     * @param string $email
     * @return mixed
     */
    public function signUp(string $email)
    {
        $message = $this->createMessage(
            'POST',
            'user/sign-up',
            [
                'accept' => true,
                'email' => $email
            ]
        );
        $message->addHeaders(['Sdk-Partner-Token' => $this->token]);
        $res = $this->sender->send($message);
        return $res;
    }

    /**
     * Login user in the mercuryo
     * @param string $email
     * @return mixed
     */
    public function signIn(string $email)
    {
        $message = $this->createMessage(
            'POST',
            'user/sign-in',
            [
                'email' => $email
            ]
        );
        $message->addHeaders(['Sdk-Partner-Token' => $this->token]);
        $res = $this->sender->send($message);
        return $res;
    }

    public function getUserData(User $user)
    {
        $message = $this->createMessage(
            'GET',
            'user/data'
        );
        $token = $user->mercuryo->bearer_token;
        $message->addHeaders(['b2b-bearer-token' => $token]);
        $res = $this->sender
            ->middleware([
                    [ReSignInMiddleware::class, 'execute'],
                    [RefreshMiddleware::class, 'execute']
            ])
            ->send($message);
        return $res;
    }

    public function refreshTokenInMiddleware(string $token)
    {
        /** @var RestMessage $message */
        $message = $this->createMessage('GET', 'user/refresh-token');
        $message->addHeaders(['b2b-bearer-token' => $token]);

        $res = $this->sender->send($message);
        return $res;
    }

    public function createMessage(string $method, string $url, array $data = [])
    {
        $request = new RestMessage($this->client);
        $request->setMethod($method)
        ->setUrl($url)
        ->setHeaders([
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'User-Agent' => $this->ua,
        ])
        ;
        if (!empty($data)) {
            $request->setData($data);
        }
        return $request;
    }
}

В методах signIn и signUp аутентификация происходит по постоянному api-keу Sdk-Partner-Token в заголовке. В методах getUserData и refreshTokenInMiddleware аутентификация происходит уже по временному jwt поэтому в случае если он истекает, то используется ReSignInMiddleware::execute middleware для его обновление в процессе запроса данных в методе MercuryoClient::getUserData(). Теперь рассмотрим как устроен класс ReSignInMiddleware который выступает в качестве промежуточного слоя.

<?php

namespace app\services\backend\finance\crypto;

use app\interfaces\finance\crypto\CryptoClientInterface;
use app\models\Email\RestMessage;
use Throwable;
use Yii;
use yii\httpclient\Response;

class ReSignInMiddleware
{
    public static function execute($message, $response, $next)
    {
        /** @var Response $response */
        $status = $response->getStatusCode();
        if ($status == 401) {
            /** @var RestMessage $message */
            /** @var MercuryoClient $s */
            try {
              //...
                $s = Yii::$container->get(CryptoClientInterface::class);
                $res = $s->signIn($mercuryo->user->email);
                $data = $res->getData();
                $message->addHeaders([
                    'b2b-bearer-token' => $data['data']['bearer_token']
                ]);
              // ...
            } catch (Throwable $e) {
                Yii::error($e->getMessage(), 'mercuryo');
                Yii::error($e->getTraceAsString(), 'mercuryo');
            }
        }
        return $next();
    }
}

Собственно говоря, если статус ответа 401, то с помощью MercuryoClient делается запрос MercuryoClient::signIn () на аутентификацию (строка 23), получается и подставляется в заголовок сообщения которое отсылалось изначально (строка 25). Тут нужно отметить важный момент, что Yii::$container->get(CryptoClientInterface::class) возвращает разные объекты MercuryoClient, а не один и тот же. Потому что иначе $this->message в классе Sender перезатирается в методе send.

Таким образом, происходит обновление временного токена (jwt) в процессе запроса данных пользователя через наш класс Sender и middleware ReSignInMiddleware.

Да, собственно сам вызов getUserData выглядит так:

<?php
$user = User::findOne($idUser);
/** @var MercuryoClient $s */
$s = Yii::$container->get(CryptoClientInterface::class);
/** @var Response $r */
$r = $s->getUserData($user);

Теги:
Хабы:
-4
Комментарии8

Публикации

Истории

Работа

PHP программист
159 вакансий

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн