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

от автора

Когда работаешь в проекте со сторонними апи предоставляющими какой-либо сервис, то необходимо делать к ним запросы с бэкенда и как по мне, делать это с бекэнда бывает не так удобно как с фронтенда. Тем более если нужное апи авторизует запросы по временному токену, который действует только какое-то время (обычно 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\MailMessage; use app\models\Email\RestMessage; use app\models\Email\UnioneRestMessage; 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\forms\Mercuryo\RefreshTokenResponse; use app\forms\Mercuryo\SignInResponse; use app\interfaces\finance\crypto\CryptoClientInterface; use app\managers\Mercuryo\MercuryoManager; use app\models\Email\RestMessage; use app\repositories\Mercuryo\MercuryoRepository; 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 */             $token      = $message->getHeaders()->get('b2b-bearer-token');             if (!$token) {                 return $next();             }             $mcrepo     = new MercuryoRepository();             $mercuryo   = $mcrepo->getByToken($token);             if (!$mercuryo) {                 return $next();             }             /** @var MercuryoClient $s */             try {                 $s = Yii::$container->get(CryptoClientInterface::class);                 $res = $s->signIn($mercuryo->user->email);                 $data = $res->getData();                 $form = new SignInResponse();                 $form->load($data['data'], '');                 $mcm = new MercuryoManager();                 $mcm->setMercuryo($mercuryo);                 $mcm->updateFromeSignInResponse($form);                 $message->addHeaders([                     'b2b-bearer-token' => $form->bearer_token                 ]);             } catch (Throwable $e) {                 Yii::error($e->getMessage(), 'mercuryo');                 Yii::error($e->getTraceAsString(), 'mercuryo');             }         }         return $next();     } }

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

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

Кроме того, для обновления jwt на половине его жизни в методе getUserData используется промежуточный слой RefreshMiddleware который обвновляет jwt делая запрос к методу /refresh-token.
Вот его реализация:

<?php  namespace app\services\backend\finance\crypto;  use app\forms\Mercuryo\RefreshTokenResponse; use app\interfaces\finance\crypto\CryptoClientInterface; use app\managers\Mercuryo\MercuryoManager; use app\models\Email\RestMessage; use app\repositories\Mercuryo\MercuryoRepository; use Yii;  class RefreshMiddleware {     public static function execute($message, $response, $next)     {         /** @var RestMessage $message */         $token      = $message->getHeaders()->get('b2b-bearer-token');         if (!$token) {             return $next();         }         $mcrepo     = new MercuryoRepository();         $mercuryo   = $mcrepo->getByToken($token);         if (!$mercuryo) {             return $next();         }         if ($mercuryo->toBeRefresh()) {             /** @var MercuryoClient $s */             try {                 $s = Yii::$container->get(CryptoClientInterface::class);                 $res = $s->refreshTokenInMiddleware($token);                 $data = $res->getData();                 $form = new RefreshTokenResponse();                 $form->load($data['data'], '');                 $mcm = new MercuryoManager();                 $mcm->setMercuryo($mercuryo);                 $mcm->updateFromRefreshTokenResponse($form);             } catch (\Throwable $e) {                 Yii::error('There is an error while refresh token', 'mercuryo');                 Yii::error($e->getMessage(), 'mercuryo');                 Yii::error($e->getTraceAsString(), 'mercuryo');                 return;             }         }         return $next();     } }

Здесь в методе $mercuryo->toBeRefresh() (строка 26) проверяется сколько осталось жить токену и если меньше 8 часов то с помощью запроса MercuryoClient::refreshTokenInMiddleware (строка 30) производится запрос нового токена (jwt). осле чего он сохраняется.

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

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


ссылка на оригинал статьи https://habr.com/ru/post/699312/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *