相比於大型框架或系統,我個人更偏好小巧、微型的可替換式套件,像 Idiorm & Paris、Phinx 這種,一來架構易於理解,抽象化的層次少,二來對專案系統而言,各功能的耦合度也降低,必要的時候可直接替換調整(例如某個套件不維護了)。
當然缺點就是什麼都要自己手動整合,獨立套件可能和核心系統的相容性沒那麼好,或是套件之間彼此有所衝突(像是相依了不同版本的套件),而且一個專案內的程式脈落會有很多不同的設計思維,需要一一去瞭解。
即使如此,目前幾大主流框架的設計思維還是十分值得參考借鏡,一套系統成長到一定規模後,勢必要藉由架構上的抽象化、介面化來做整理和增加彈性,這是不可避免的發展路徑。
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 所指定的,具體的過程如下:
- 程式中存取
app['auth']。 app自我檢查是否auth已經建立,如果尚未建立則進行實例化。- 因為 Lumen Application — Laravel\Lumen\Application 當中有綁定
auth為特殊關鍵字,要進行auth的實例化時會觸發registerAuthBindings函式。 registerAuthBindings函式將核心服務 Illuminate\Auth\AuthServiceProvider 進行註冊。- Illuminate\Auth\AuthServiceProvider 在 register function 中,透過單例模式
app->singleton將 Illuminate\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 函式會收到
$request和userProvider兩個傳入變數,並要求回傳 可用的使用者物件(不是回傳驗證結果而已)。 - TokenGuard:同樣基於連線請求,針對傳入內容進行驗證的模式,例如 header 中是否帶有特定的 token 或 password。
- SessionGuard:依據 Session(和 Cookie)的資料進行驗證模式,也就是一般 PHP 程式中,紀錄 / 識別使用者登入所採用的方式。
當 app['auth'] — AuthManager 實例化以後,我們即可透過它來取得上述的 Guard 驗證物件(或是你的自定義 Guard):
- 呼叫
app['auth']->guard('XXX'),如果對應名稱 XXX 的 Guard 物件已存在則回傳,若否,準備建立 Guard 物件;要注意的是沒有帶入名稱時,會自動使用 config/auth.php 中的defaults.guard。 - 建立 Guard 物件前,會先檢查 config/auth.php 的
guards裡是否有 XXX Guard 的相關設定,沒有的話會拋出錯誤。 - 綜合 XXX Guard 的設定資料,依序檢查此 Guard 是否有:
- 自定義的 callback 建立函式:在程式中經由
app['auth']->extend()以 callback 進行定義的建立函式,必須同時在設定檔中設定此定義的名稱。 - AuthManager 內的 method 驅動函式:
- 如果有自行擴充 AuthManager 的話,可以定義 createXXXDriver 的 method 來驅動 XXX Guard。
- 內建 Guard 的 預設的驅動函式 也是同樣方式,例如 SessionGuard 是 createSessionDriver,TokenGuard 則是 createTokenDriver。
- 自定義的 callback 建立函式:在程式中經由
- 呼叫 建立函式 / 驅動函式 實例化目標 Guard,並將物件以 XXX 為索引存入 AuthManager 自身
guards array裡。 - 回傳 Guard 物件。
從上面的流程可以知道,如果你要自定一個 Guard,最簡單的方式就是透過 app['auth']->extend() 定義你 Guard 的啟動函式,並在 config/auth.php 中寫好設定。
回頭看 app['auth']->viaRequest,其實 viaRequest 是基於 RequestGuard 的 extend() 延伸;白話來講,就是幫你建立 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();
}
});
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 之間的關聯。謝謝各位的閱讀,我們下次見。


