Lumen Auth、Guard、User 身份驗證詳解(三)- 全部的流程都解開了!(上)

Wake Liu
興趣使然的程式猿
8 min readAug 10, 2019
Photo by Neil Martin on Unsplash
Lumen Auth、Guard、User 身份驗證詳解 文章目錄:(一)Authentication 文件忙什麼
(二)你的架構不是你的架構
(三)全部的流程都解開了!(上)

相比於大型框架或系統,我個人更偏好小巧、微型的可替換式套件,像 Idiorm & ParisPhinx 這種,一來架構易於理解,抽象化的層次少,二來對專案系統而言,各功能的耦合度也降低,必要的時候可直接替換調整(例如某個套件不維護了)。

當然缺點就是什麼都要自己手動整合,獨立套件可能和核心系統的相容性沒那麼好,或是套件之間彼此有所衝突(像是相依了不同版本的套件),而且一個專案內的程式脈落會有很多不同的設計思維,需要一一去瞭解。

即使如此,目前幾大主流框架的設計思維還是十分值得參考借鏡,一套系統成長到一定規模後,勢必要藉由架構上的抽象化、介面化來做整理和增加彈性,這是不可避免的發展路徑。

Auth、Guard、User 的流程實務

回到主題,我們接下來要詳細走過一次身份驗證的完整旅程,圖上我補充了 Lumen 和 AuthServiceProvider 兩部份,讓關聯性更加清楚。

Lumen → ServiceProvider → AuthManager

要在 Lumen 系統啟用身份驗證功能,我們需要將先 AuthServiceProvider 註冊到 Lumen Application — 也就是 $app 當中,這同時也是流程的起點:

$app->register (\App\Providers\AuthServiceProvider::class);

AuthServiceProvider 是怎麼將相關服務綁定到系統中的?檢視原始碼,可以從 boot function 看到以下:

$this->app['auth']->viaRequest ('api', function ($request) {
...
});

首先這裡要求取得 $this->app['auth']app['auth'] 是一個本來不存在的物件,經由存取時的觸發,進而實例化了 AuthManager,但為什麼是 Illuminate\Auth\AuthManager 呢?這是由核心程式 Illuminate\Auth\AuthServiceProvider 所指定的,具體的過程如下:

  1. 程式中存取 app['auth']
  2. app 自我檢查是否 auth 已經建立,如果尚未建立則進行實例化。
  3. 因為 Lumen Application — Laravel\Lumen\Application 當中有綁定 auth 為特殊關鍵字,要進行 auth 的實例化時會觸發 registerAuthBindings 函式。
  4. registerAuthBindings 函式將核心服務 Illuminate\Auth\AuthServiceProvider 進行註冊。
  5. Illuminate\Auth\AuthServiceProviderregister function 中,透過單例模式 app->singletonIlluminate\Auth\AuthManager 類別實例化並賦予給 app['auth']

所以除了呼叫 app['auth'] 和簡單加工, App\Providers\AuthServiceProvider 沒特別載入什麼服務內容
,主要還是由核心的 auth 相關綁定進行的處理。

後面一段程式碼 app['auth']->viaRequest 因為和 Guard 有關,我們在下節一併說明。

AuthManager → XXX (Guard) → GuardDriver → Guard

那麼 AuthManager 是如何和 Guard 進行互動的?在 Lumen 系統中,共內建了三種 Guard 驗證類別:

  • RequestGuard:基於連線請求,提供擴充用的驗證模式,必須在物件建立時傳入 callback 函式;此 cb 函式會收到 $requestuserProvider 兩個傳入變數,並要求回傳 可用的使用者物件 (不是回傳驗證結果而已)。
  • TokenGuard:同樣基於連線請求,針對傳入內容進行驗證的模式,例如 header 中是否帶有特定的 token 或 password。
  • SessionGuard:依據 Session(和 Cookie)的資料進行驗證模式,也就是一般 PHP 程式中,紀錄 / 識別使用者登入所採用的方式。

app['auth']AuthManager 實例化以後,我們即可透過它來取得上述的 Guard 驗證物件(或是你的自定義 Guard):

  1. 呼叫 app['auth']->guard ('XXX') ,如果對應名稱 XXX 的 Guard 物件已存在則回傳,若否,準備建立 Guard 物件;要注意的是沒有帶入名稱時,會自動使用 config/auth.php 中的 defaults.guard
  2. 建立 Guard 物件前,會先檢查 config/auth.phpguards 裡是否有 XXX Guard 的相關設定,沒有的話會拋出錯誤。
  3. 綜合 XXX Guard 的設定資料,依序檢查此 Guard 是否有:
    ● 自定義的 callback 建立函式:在程式中經由 app['auth']->extend () 以 callback 進行定義的建立函式,必須同時在設定檔中設定此定義的名稱。
    ● AuthManager 內的 method 驅動函式:如果有自行擴充 AuthManager 的話,可以定義 createXXXDriver 的 method 來驅動 XXX Guard。內建 Guard 的 預設的驅動函式 也是同樣方式,例如 SessionGuardcreateSessionDriver,TokenGuard 則是 createTokenDriver
  4. 呼叫 建立函式 / 驅動函式 實例化目標 Guard,並將物件以 XXX 為索引存入 AuthManager 自身 guards array 裡。
  5. 回傳 Guard 物件。

從上面的流程可以知道,如果你要自定一個 Guard ,最簡單的方式就是透過 app['auth']->extend () 定義你 Guard 的啟動函式,並在 config/auth.php 中寫好設定。

回頭看 app['auth']->viaRequest ,其實 viaRequest 是基於 RequestGuardextend () 延伸 ;白話來講,就是幫你建立 RequestGuard 物件,並且提供傳入 user callback 的函式。我們看一下這兩段程式:

AuthServiceProvider 的 boot function

$this->app['auth']->viaRequest ('api', function ($request) {
if ($request->input ('api_token')) {
return User::where('api_token'
, $request->input('api_token'))->first();
}
});

AuthManager 的 viaRequest

public function viaRequest ($driver, callable $callback) {
return $this->extend ($driver, function () use ($callback) {
$guard = new RequestGuard ($callback
, $this->app['request']
, $this->createUserProvider ());
$this->app->refresh ('request', $guard, 'setRequest'); return $guard;
});
}

AuthServiceProvider 啟動時, viaRequest () 經由 extend () 定義了名為 api 的 建立 / 驅動函式,並且傳入一個 從連線請求中的 api_token 比對資料庫取得使用者 的 callback 作為 RequestGuard 的建構參數,等下一次呼叫 app['auth']->guard ('api') 時就會進行實例化,並按 callback 回傳使用者物件。

結語

AuthManager 大概是身份驗證架構中,扮演角色最複雜的物件了;除了原本功能外,它還透過 __call 實做了對應 Guard 的動態呼叫,也就是說可以直接 app['auth']->guardFunc () 替代 app['auth']->guard ()->guardFunc (),真的讓人很難回追架構啊…

這系列文感覺越寫越多,下一篇我們會繼續深入 AuthManager 和 UserProvider 之間的關聯。謝謝各位的閱讀,我們下次見。

--

--