API 開發中可選擇傳遞 token 介面遇到的一個坑

seth-shi發表於2018-06-24
  1. 在做 API 開發時,不可避免會涉及到登入驗證,我使用的是jwt-auth
  2. 在登入中會經常遇到一個token過期的問題,在config/jwt.php預設設定中,這個過期時間是一個小時,不過為了安全也可以設定更小一點,我設定了為五分鐘。
  3. 五分鐘過期,如果就讓使用者去登入,這種體驗會讓使用者直接拋棄你的網站,所以這就會使用到重新整理token這個功能
  4. 正常情況下是寫一個重新整理token的介面,當過期的時候前端把過期的token帶上請求這個介面換取新的token
  5. 不過為了方便前端也可以使用後端重新整理返回,直至不可重新整理,我用的就是這個方法:使用 Jwt-Auth 實現 API 使用者認證以及無痛重新整理訪問令牌
  6. 而坑就是這樣來的,
    • 在必須需要登入驗證的介面設定重新整理token
<?php

namespace App\Http\Middleware;

use App\Services\StatusServe;
use Closure;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;

class CheckUserLoginAndRefreshToken extends BaseMiddleware
{
    /**
     * 檢查使用者登入,使用者正常登入,如果 token 過期
     * 重新整理 token 從響應頭返回
     *
     * @param         $request
     * @param Closure $next
     * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response
     * @throws JWTException
     */
    public function handle($request, Closure $next)
    {
        /****************************************
         * 檢查token 是否存在
         ****************************************/
        $this->checkForToken($request);

        try {
            /****************************************
             * 嘗試透過 tokne 登入,如果正常,就獲取到使用者
             * 無法正確的登入,丟擲 token 異常
             ****************************************/
            if ($this->auth->parseToken()->authenticate()) {
                return $next($request);
            }
            throw new UnauthorizedHttpException('jwt-auth', 'User not found');

        } catch (TokenExpiredException $e) {
            try {
                /****************************************
                 * token 過期的異常,嘗試重新整理 token
                 * 使用 id 一次性登入以保證此次請求的成功
                 ****************************************/
                $token = $this->auth->refresh();
                $id = $this->auth
                    ->manager()
                    ->getPayloadFactory()
                    ->buildClaimsCollection()
                    ->toPlainArray()['sub'];

                auth()->onceUsingId($id);
            } catch (JWTException $e) {
                /****************************************
                 * 如果捕獲到此異常,即代表 refresh 也過期了,
                 * 使用者無法重新整理令牌,需要重新登入。
                 ****************************************/
                throw new UnauthorizedHttpException('jwt-auth', $e->getMessage(), null, StatusServe::HTTP_PAYMENT_REQUIRED);
            }
        }

        // 在響應頭中返回新的 token
        return $this->setAuthenticationHeader($next($request), $token);
    }
}
  • 而有些頁面,比如文章列表頁面,這個介面登入與不登入皆可訪問,不過登入的時候可以在頁面上顯示是否點讚了這篇文章。所以這個介面直接使用的是jwt-auth預設的option中介軟體
<?php

/*
 * This file is part of jwt-auth.
 *
 * (c) Sean Tymon <tymon148@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Tymon\JWTAuth\Http\Middleware;

use Closure;
use Exception;

class Check extends BaseMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     *
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if ($this->auth->parser()->setRequest($request)->hasToken()) {
            try {
                $this->auth->parseToken()->authenticate();
            } catch (Exception $e) {

            }

        }

        return $next($request);
    }
}
  1. 一開始也沒有發現問題,直到測試的時候,發現文章列表頁面點贊過的文章,過了一段時間再重新整理的時候發現不顯示已點贊,但是進入個人中心的已點贊文章可以看到。
  2. 剛開始測試沒找出原因,直接暴力除錯程式碼,發現沒獲取到登入使用者,一想不對呀,已經傳token為何獲取不到。經過發現,去到個人中心,再回到新聞列表頁就可以正常顯示,過了一段時間又不顯示了。
  3. 經過這一輪之後,大概明白,在新聞列表頁時,token已經過期,但是當時圖方便用的jwt-auth預設的中介軟體,不會重新整理token,所以這個介面獲取不到登入的使用者。當進入個人中心,發現當前token已經過期,後臺重新整理token返回,這時候再回到文章列表頁就可以得到正常的資料,一段時間後,token又失效了,所以有無法看到點贊過的文章
  4. 解決方法,自己寫一個option中介軟體,當存在token的時候,也需要做token重新整理處理。
<?php

namespace App\Http\Middleware;

use Closure;
use Exception;

class Check extends BaseMiddleware
{

    public function handle($request, Closure $next)
    {
        if ($this->auth->parser()->setRequest($request)->hasToken()) {
            try {
                $this->auth->parseToken()->authenticate();
            } catch (TokenExpiredException $e) {
                // 此處做重新整理 token 處理
                // 具體程式碼可以參考必須需要登入驗證的介面
                // 在響應頭中返回新的 token
                return $this->setAuthenticationHeader($next($request), $token);
            } catch (Exception $e) {

            }

        }

        return $next($request);
    }
}

問題解決。
最後說一個併發會出現的問題:

# 當前 token_1 過期,先發起 a 請求,之後馬上發起 b 請求
# a 請求到伺服器,伺服器判斷過期,重新整理 token_1
# 之後返回 token_2 給 a 請求響應
# 這時候遲一點的 b 請求用的還是 token_1 
# 伺服器已經將此 token_1 加入黑名單,所以 b 請求無效
       token_1         重新整理返回 token_2
a 請求 --------> server -------> 成功
       token_1         過期的 token_1,應該使用 token_2
b 請求 --------> server ------> 失敗

jwt-auth已經想到這種情況,我們只需要設定一個黑名單寬限時間即可

我設定為5秒,就是當token_1過期了,你還能繼續使用token_1操作5秒時間

原文地址

本作品採用《CC 協議》,轉載必須註明作者和本文連結
當神不再是我們的信仰,那麼信仰自己吧,努力讓自己變好,不辜負自己的信仰!

相關文章