在 Laravel 實現自動 Refresh JWT 機制

簡介 JWT

JSON Web Token (JWT) 是由 Auth0 於 2015 年所提構出的一個新 Token 機制,嚴格來說 JWT 並不是一個新的技術或軟體,而是一套規範(RFC-7519)。基本上 JWT 包含以下三種資訊:

  • Header
{"alg":"HS256","typ":"JWT"}
  • Payload
{"loggedInAs":"admin","iat":1422779638}
  • Signature
key           = 'secretkey'
unsignedToken = encodeBase64Url(header) + '.' + encodeBase64Url(payload)
signature     = HMAC-SHA256(key, unsignedToken) 

以上資訊最終會被以 . 的符號組成一段 JWT Token

encodeBase64Url(header) + '.' + encodeBase64Url(payload) + '.' + encodeBase64Url(signature)

由於使用 JWT 來進行驗證並不需要將資訊存放在 Server 當中,因此更易於 Server 橫向擴展,除此之外還具有以下優點:

  • Token 本身即能表示過期時間資訊
  • 即便在禁止使用 Cookie 的裝置下也能作用
  • 防止 CSRF 攻擊

同時有另外一派的人呼籲停止使用 JWT 這樣的機制,並提出 JWT 在各方面優點的錯誤假設與缺失,由於探討 JWT 使否為更好的選擇不在本文的探討範圍,並且筆者認為使用 Session 或是 JWT 本來就沒有絕對的對錯,只有瞭解各自的優缺點並且在正確的使用場景運用它才能解決實際的問題,附上探討相關議題的原文連結:
http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/

為什麼需要 Refresh JWT Token?

因為 JWT Token 本身帶有過期時效的資訊,在安全性的考量下,配發一個長效期的 Token(個人認為超過一天就算是長效期了)明顯不是一個明智的選擇,所以通常會配發短效期 Token(例如一個小時)並且搭配 Refresh 的機制來達成交換 Token 的目的。

簡單來說,就是拿舊的 Token 向 Server 交換新的 Token,並且交換 Token 的過程中 Server 也能趁此行為來更新帶在 Payload 中的資訊。

以最近幾年非常熱門的 SPA 架構網站為例,如何在 App 和 Server 溝通的過程來達成 Server 一但偵測到 Token 過期便自動換發新的 Token 並且通知 App 更新資訊就需要一點技巧了,來假設以下的使用情境:

  • Server 在偵測到 Token 過期時會自動觸發 Refresh Token 機制
  • 經過 Refresh Token,舊的 Token 會被 Blacklist 掉,新的 Token 會被產生並通知 App
  • 理想情況下 App 可以透過上面的機制無限交換 Token 不會出錯
  • 但是 App 在不佳的網路情況下或是平行的 Request 下會有問題
  • 雖然 App 成功更新 Token 了,但在更新前被送出的 Request 依然是帶舊的 Token

為了解決以上的情境,App 可以選擇在 Server 告知 Token 已經被黑名單的情形下使用新的 Token 再重新向 Server 送一次 Request 來解決這個問題,但是筆者在這裡分享一個 App 端完全不需要做重送 Request 的機制,解決思路大概如下圖所示:

意即 Server 有能力在緩衝時間內允許舊的 Token 存取,並且不會重複配發新的 Token,所以在 App 中便可以無感知的來使用 Token 存取 Server 資源,不用處理 Request 因為被 Server 拒絕而需要重送的機制。

當然這樣的情境有很多面向能去解決這個問題,這裡只是提供一個參考的解決方案。

在 Laravel 中實現自動 Refresh JWT 機制

實作原理

透過 Middleware 的方式檢查 Token 是否有過期,如果過期就自動換發一個新的 Token 帶在 Response Header 中回傳,並將該次的換發過程利用 Cache 記錄下來(預設紀錄一分鐘)。如此在這一分鐘內如果使用相同的舊 Token 來存取則暫時允許存取並一樣回傳已配發過的新 Token。

使用限制:由於此解決方案需要透過 Cache 來記錄資訊,若是有多 Server 的狀況需要像使用 Session 一樣的解決方案來共用 Cache 資訊,例如:將 Cache Server 獨立出來。

安裝 Unisharp/laravel-jwt Package

該 package 是基於 tymon/jwt-auth 去進行包裝的,需注意版本是否和你原先的 tymon/jwt-auth 版本有衝突。

安裝

  • 透過 composer 安裝 unisharp/laravel-jwt
composer require unisharp/laravel-jwt
  • 新增 Service Provider
Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
Unisharp\JWT\JWTServiceProvider::class,

Lumen 請使用 Tymon\JWTAuth\Providers\LumenServiceProvider::class,,另外請注意載入順序,Tymon 的 ServiceProvider 一定要較早載入

你可以透過以下的設定檔來更改 Server 端允許的緩衝時間

php artisan vendor:publish --provider="Unisharp\JWT\JWTServiceProvider"

使用前記得檢查是否有產生 JWT Key

$ php artisan jwt:secret

使用方式

  • 編輯 Laravel 中的 config/auth.php 並將 jwt-auth 設定為預設 driver
// config/auth.php
'guards' => [
    'api' => [
        'driver' => 'jwt-auth',
        'provider' => 'users'
    ],
    
    // ...
]
  • 使用具有自動 Refresh 機制的 JWT Middleware
Route::get('api/content', ['middleware' => 'laravel.jwt', 'uses' => 'ContentController@content']);

或是

<?php

namespace App\Http\Controllers;
class ContentController extends Controller
{
    public function __construct() 
    {
        $this->middleware('laravel.jwt');
    }
}

該 Middleware 會在偵測到 Token 過期時自動核發一組新的 Token,並將新的 Token 資訊帶在 Response 的 Ahthorization Header 中