РЕДАКТИРОВАТЬ:
Прочитайте дискуссию об ошибке: https://github.com/tymondesigns/jwt-auth/issues/83
МОЙ ОРИГИНАЛЬНЫЙ ВОПРОС:
Я реализую с jwt-auth моими защищенными ресурсами, для которых требуется аутентифицированный пользователь со следующим кодом:
Route::group(['middleware' => ['before' => 'jwt.auth', 'after' => 'jwt.refresh']], function() { // Protected routes });
Когда пользователь «вписывается» в API, создается токен авторизации и отправляется в ответ на заголовок авторизации в клиентское приложение, вызывающее ресурс. Таким образом, клиентские приложения, перехватывая токен авторизации в заголовке любого ответа, устанавливают переменную / session / whatever с этим значением токена, чтобы снова отправить API в следующий запрос.
Первый запрос на защищенный ресурс после «входа» отлично работает, но следующий запрос клиентского приложения к API с обновленным токеном дает следующую ошибку (API монтирует все ответы в формате json):
{ "error": "token_invalid" }
Что может случиться с обновленными жетонами? Моя реализация токена обновления (установленная как промежуточное ПО) неверна? Или нет необходимости вручную обновлять весь токен авторизации, который поставляется с запросами клиентских приложений?
ОБНОВИТЬ:
Я обновляю промежуточное программное обеспечение jwt-auth RefreshToken как предложение здесь , но token_invalid
сохраняется.
BUG:
Думаю, я нашел, что происходит. Обратите внимание, что в методе обновления старый токен добавляется в кэш-память черного списка:
// Tymon\JWTAuth\JWTManager public function refresh(Token $token) { $payload = $this->decode($token); if ($this->blacklistEnabled) { // invalidate old token $this->blacklist->add($payload); } // return the new token return $this->encode( $this->payloadFactory->setRefreshFlow()->make([ 'sub' => $payload['sub'], 'iat' => $payload['iat'] ]) ); }
И обратите внимание, что при добавлении метода черного списка ключ – это параметр jti из старой полезной нагрузки token:
// Tymon\JWTAuth\Blacklist public function add(Payload $payload) { $exp = Utils::timestamp($payload['exp']); // there is no need to add the token to the blacklist // if the token has already expired if ($exp->isPast()) { return false; } // add a minute to abate potential overlap $minutes = $exp->diffInMinutes(Utils::now()->subMinute()); $this->storage->add($payload['jti'], [], $minutes); return true; }
Таким образом, когда вызывается метод blacklist, старый токен jti param тот же, что и новый, поэтому новый токен находится в черном списке:
// Tymon\JWTAuth\Blacklist public function has(Payload $payload) { return $this->storage->has($payload['jti']); }
Если вам не нужны функции черного списка, просто установите значение false в файле конфигурации jwt.php. Но я не могу сказать, подвергает ли она некоторую уязвимость системы безопасности.
Прочитайте дискуссию об ошибке: https://github.com/tymondesigns/jwt-auth/issues/83
Когда я получаю эту проблему, решение, которое я нашел для моего проекта, заключалось в создании нового токена с данными из старого токена для каждого нового запроса.
Мое решение, которое работает для меня, плохое, уродливое и может генерировать больше проблем, если у вас много асинхронных запросов, а ваш сервер API (или бизнес-ядра) работает медленно.
На данный момент работает, но я буду исследовать больше этой проблемы, потому что после версии 0.5.3 проблема продолжается.
Например:
Запрос 1 (GET / login):
Some guest data on token
Запрос 2 (ответ POST / логин):
User data merged with guest data on old token generating a new token
Пример процедурного кода (вы можете сделать лучше =)), вы можете запустить это на route.php из маршрутов, я говорю, что это уродливый ха-ха:
// ---------------------------------------------------------------- // AUTH TOKEN WORK // ---------------------------------------------------------------- $authToken = null; $getAuthToken = function() use ($authToken, $Response) { if($authToken === null) { $authToken = JWTAuth::parseToken(); } return $authToken; }; $getLoggedUser = function() use ($getAuthToken) { return $getAuthToken()->authenticate(); }; $getAuthPayload = function() use ($getAuthToken) { try { return $getAuthToken()->getPayload(); } catch (Exception $e) { return []; } }; $mountAuthPayload = function($customPayload) use ($getLoggedUser, $getAuthPayload) { $currentPayload = []; try { $currentAuthPayload = $getAuthPayload(); if(count($currentAuthPayload)) { $currentPayload = $currentAuthPayload->toArray(); } try { if($user = $getLoggedUser()) { $currentPayload['user'] = $user; } $currentPayload['isGuest'] = false; } catch (Exception $e) { // is guest } } catch(Exception $e) { // Impossible to parse token } foreach ($customPayload as $key => $value) { $currentPayload[$key] = $value; } return $currentPayload; }; // ---------------------------------------------------------------- // AUTH TOKEN PAYLOAD // ---------------------------------------------------------------- try { $getLoggedUser(); $payload = ['isGuest' => false]; } catch (Exception $e) { $payload = ['isGuest' => true]; } try { $payload = $mountAuthPayload($payload); } catch (Exception $e) { // Make nothing cause token is invalid, expired, etc., or not exists. // Like a guest session. Create a token without user data. }
Некоторые маршруты (простой пример для сохранения пользовательского мобильного устройства):
Route::group(['middleware' => ['before' => 'jwt.auth', 'after' => 'jwt.refresh']], function () use ($getLoggedUser, $mountAuthPayload) { Route::post('/session/device', function () use ($Response, $getLoggedUser, $mountAuthPayload) { $Response = new \Illuminate\Http\Response(); $user = $getLoggedUser(); // code to save on database the user device from current "session"... $payload = app('tymon.jwt.payload.factory')->make($mountAuthPayload(['device' => $user->device->last()->toArray()])); $token = JWTAuth::encode($payload); $Response->header('Authorization', 'Bearer ' . $token); $responseContent = ['setted' => 'true']; $Response->setContent($responseContent); return $Response; }); });