解決使用jwt刷新token帶來的問題

先後端分離,使用token的方式校驗用戶信息,我選擇了jwt,使用的教程在網上能夠找到不少,不作介紹。php

這裏說明一個使用過程當中,最重要的的一個環節刷新token帶來的問題。前端

業務要達到的目標:git

用戶登陸一次以後,前端保存token,後面每次向後端請求的時候,header都帶上authorization信息,後端從請求中解析token,根據token驗證用戶信息,返回相應的信息。github

相信大部分看過文檔並開始使用的同窗都已經走通到這裏了,下面是入坑的開始:redis

1. 產品要求json

半個月內免登錄,這裏就要使用到了refreshToken了,jwt設計思想很到位:設置發給前端的token一個有效期,好比2個小時,2個小時候前端發來的token就會失效,這個時候咱們根據發來的token判斷下,若是這個token在2個小時外,並在刷新token的有效期內(好比半個月內),那麼咱們在給前端返回數據的時候返回一個新token,前端接到這個token存儲起來,當再次請求的時候,發送新的token,如此周而復始,只要你在半個月內沒有間斷去進入系統,那麼徹底不須要去進行登陸的操做。

2. 問題
1)如何將新的token發給前端比較好?後端

這個問題答案簡單,在response 的header中設置authorization。
   關鍵點:後端通常使用的域名是二級域名好比個人是api.xx.com,會和前端產生一個跨域的影響,請記得必定要設置
   `$response->headers->set('Access-Control-Expose-Headers', 'Authorization');`
   設置跨域的時候還要設置一個Cache-Control,這個東西出現的問題真的是莫名其妙,坑了我好久..
   `$response->headers->set('Cache-Control', 'no-store'); // 無的話會致使前端從緩存獲取頭token`

2) 通常是在中間件中刷新token,當前請求繼續走,如何在controller中須要根據token調取登陸用戶信息?api

一會兒可能沒說明問題,簡單理解爲:token已經刷新了,那麼當前token確定失效了,繼續在controller利用請求中的token確定會報token失效的錯誤,這裏須要將新token帶到後面的程序處理中,我這裏更改了當前請求頭,將newToken替換了request header中的Authorization。

3) 併發請求。也就是2個小時候以後,同一個頁面發來了2個請求,這個很正常,好比一個請求列表數據,一個請求搜索的表單,由於token都已失效,那麼難道返回2個新的token回去?跨域

這個問題找了在github裏面看到了issue可是無人回答,jwt確定不會發兩個新的token回去的,那麼確定會有一個token不只是失效了,刷新當前token以後,產生新的token,舊token加入到了backlist中了,沒法使用,那麼另一個請求天然沒法成功。我這裏使用Redis解決的,將舊token做爲鍵,新token做爲值,設置一個30秒過時的時間。當第二個請求來的時候,已經知道token在backlist中了,咱們能夠去redis查詢下是否存在這麼個舊token,存在的話放行。

3. 關鍵中間件代碼緩存

<?php
   
   namespace App\Http\Middleware;
   
   use Closure;
   use JWTAuth;
   use Tymon\JWTAuth\Exceptions\JWTException;
   use Tymon\JWTAuth\Exceptions\TokenExpiredException;
   use Tymon\JWTAuth\Exceptions\TokenInvalidException;
   use Illuminate\Support\Facades\Redis;
   
   class GetUserFromToken
   {
       
       public function handle($request, Closure $next)
       {
           $newToken = null;
           $auth = JWTAuth::parseToken();
           if (! $token = $auth->setRequest($request)->getToken()) {
               return response()->json([
                   'code' => '2',
                   'msg' => '無參數token',
                   'data' => '',
               ]);
           }
   
           try {
               $user = $auth->authenticate($token);
               if (! $user) {
                   return response()->json([
                       'code' => '2',
                       'msg' => '未查詢到該用戶信息',
                       'data' => '',
                    ]);
               }
               $request->headers->set('Authorization','Bearer '.$token);
           } catch (TokenExpiredException $e) {
               try {
                   sleep(rand(1,5)/100);
                   $newToken = JWTAuth::refresh($token);
                   $request->headers->set('Authorization','Bearer '.$newToken); // 給當前的請求設置性的token,以備在本次請求中須要調用用戶信息
                   // 將舊token存儲在redis中,30秒內再次請求是有效的
                   Redis::setex('token_blacklist:'.$token,30,$newToken);
               } catch (JWTException $e) {
                   // 在黑名單的有效期,放行
                   if($newToken = Redis::get('token_blacklist:'.$token)){
                       $request->headers->set('Authorization','Bearer '.$newToken); // 給當前的請求設置性的token,以備在本次請求中須要調用用戶信息
                       return $next($request);
                   }
                   // 過時用戶
                   return response()->json([
                       'code' => '2',
                       'msg' => '帳號信息過時了,請從新登陸',
                   ]);
               }
           } catch (JWTException $e) {
               return response()->json([
                   'code' => '2',
                   'msg' => '無效token',
                   'data' => '',
                ]);
           }
           $response = $next($request);
   
           if ($newToken) {
               $response->headers->set('Authorization', 'Bearer '.$newToken);
           }
   
           return $response;
       }
   }

一成天的時間耗在這裏了,實踐纔會發現問題,累並快樂着解決了^_^

=====================割了一了白了(2019)===================

關於第三個問題,當時寫的時候就感受很噁心,做者已經出了一個新版本(1.0.0-rc.1),config裏面多了一項配置
'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 60)
當多個併發請求使用相同的JWT進行時,因爲 access_token 的刷新 ,其中一些可能會失敗,以秒爲單位設置請求時間以防止併發的請求失敗。
你們能夠享用這個新版,能夠很好解決這個問題。中間件更新了部分:

try {
    // 刷新token,超時刷新將會去catch
    $refresh = JWTAuth::parseToken()->refresh();
    $user = $auth->authenticate($refresh);
    // 生成新token(上面的$refresh也是合法的刷新token,這裏若是直接用,2次刷新以後再沒法繼續得到,親測)
    $newToken = JWTAuth::fromUser($user);
    // 給當前的請求設置性的token,以備在本次請求中須要調用用戶信息
    $request->headers->set('Authorization','Bearer '.$newToken);
}
相關文章
相關標籤/搜索