在Laravel/Lumen
框架中,使用者的登入/註冊的認證基本都已經封裝好了,開箱即用。而登入/註冊認證的核心就是:
- 使用者的註冊資訊存入資料庫(登記)
- 從資料庫中讀取資料和使用者輸入的對比(認證)
上述兩步是登入/註冊的基本,可以看到都會涉及到資料庫的操作,這兩步框架底層已經幫我們做好了,而且考慮到了很多情況,比如使用者認證的資料表不是user表
而是admin_user
,認證欄位是phone
而不是email
,等等一些問題都是Guard
所要解決的,通過Guard
可以指定使用哪個資料表什麼欄位等,Guard
能非常靈活的構建一套自己的認證體系。
通俗地講,就是這樣:Guard
就像是小區的門衛大叔,冷酷無情,不認人只認登記資訊。進小區之前大叔需要先檢查你的身份,驗證不通過大叔就不讓你進去。如果是走路/騎車進去,大叔1需要檢查你的門禁卡,他拿出記錄了小區所有業主門禁卡資訊的本子檢視你這個門禁卡資訊有沒有在這個本子上;如果你開車進去,大叔2就從記錄了所有業主車牌號的本子中檢查你的車牌號,所以新業主要小區了需要告知門衛大叔們你的門禁卡資訊或者車牌號,要不然大叔2不讓你進。如果是物業管理員要進小區,門衛大叔3也只認登記資訊,管理員出示他的管理員門禁卡,門衛大叔就會檢查記錄了管理員門禁卡資訊的本子。
上面講的對應了框架中的多使用者認證:
- 走路/騎車的人 -> 門禁卡
- 開車的人 -> 車牌號
- 物業管理員 -> 門禁卡
門禁卡和車牌號都是不同的認證方式,而門衛大叔檢視的本子就對應了不同資料庫中的使用者資訊,這樣講是不是更容易理解了。
Lumen/Laravel
中以中介軟體(Middleware
)的方式提供了非常靈活的認證,通過簡單的配置就可以切換多個認證。
注:本文所講的都是
Lumen
的程式碼,是Laravel
精簡版,內部實現原理都大差不差本文所使用的是:Laravel 7.29
說了這麼多,附上一張手工製作的流程圖:
從圖中可以看到,一個Guard
會涉及到三個部分,分別是:
Guard
實現本身User Provider
使用者提供者,指定哪個資料表以什麼方式獲取(eloquent/database
)Authenticatable
介面規定那些東西可以被認證,就是實現它的介面嘛
深入底層程式碼之前,先從配置檔案講起。認證的配置主要在/config/auth.php
中,裡面可以定義各種認證的門衛大叔(guard):
// /config/auth.php
'guards' => [
'user' => [
'driver' => 'session',
'provider' => 'users',
],
'admin' => [
'driver' => 'token',
'provider' => 'admin_users',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
// 'table' => 'user'
],
'admin_users' => [
'driver' => 'eloquent',
'model' => App\Models\AdminUser::class,
],
],
配置中定義了兩個門衛user
和admin
,driver
欄位設定門衛的認證系統,預設提供兩種sessesion
和token
,provider
定義的就是上面說的本子,儲存所有的認證使用者,provider
下面的drive
定義認證使用者如何獲取,有兩種方式database
和eloquent
方式,一般都是用第二種,model
定義eloquent
方式使用的資料模型,如果driver
是database
,就要設定table
指定資料庫表。如果沒有程式碼中沒有指定用哪個門衛,就會使用預設的門衛大爺:
'defaults' => [
'guard' => 'users',
'passwords' => 'users',
],
我們以Laravel
中auth
中介軟體例子來簡單說一下:
Route::get('/user/profile', 'UserController@profile')->middleware('auth');
當發起/user/profile
這個請求時,在進入UserController::profile
方法前,會呼叫auth
中介軟體,auth
定義在\app\Http\Kernel.php
中:
// \app\Http\Kernel.php
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
...
];
對應處理指令碼是\App\Http\Middleware\Authenticate::class
,
// \app\Http\Middleware\Authenticate.php
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string[] ...$guards
* @return mixed
*
* @throws \Illuminate\Auth\AuthenticationException
*/
public function handle($request, Closure $next, ...$guards)
{
$this->authenticate($request, $guards);
return $next($request);
}
Laravel
中中介軟體的處理入口都是handle
方法,引數中會一陣列形式傳過來多個使用的guard
,比如這樣:
Route::get('/user/profile', 'UserController@profile')->middleware('auth:session,foo,bar');
middleware()
中冒號前後分別是中介軟體和引數。
handle
方法很簡單嘛,就是呼叫了authenticate()
:
// \Illuminate\Auth\Middleware\Authenticate.php
/**
* Determine if the user is logged in to any of the given guards.
*
* @param \Illuminate\Http\Request $request
* @param array $guards
* @return void
*
* @throws \Illuminate\Auth\AuthenticationException
*/
protected function authenticate($request, array $guards)
{
if (empty($guards)) {
$guards = [null];
}
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}
}
$this->unauthenticated($request, $guards);
}
authenticate()
方法遍歷傳過來的guard
,然後check()
,只要滿足其中一個,就直接返回,否則就會丟擲AuthenticationException
異常。
⚠️注意
$this->auth->guard($guard)->check()
這個是關鍵,它是通過在auth
屬性上鍊式呼叫的,我們來一步一步分析下:
// \Illuminate\Auth\Middleware\Authenticate.php
namespace Illuminate\Auth\Middleware;
use Closure;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Auth\Factory as Auth;
use Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests;
class Authenticate implements AuthenticatesRequests
{
/**
* The authentication factory instance.
*
* @var \Illuminate\Contracts\Auth\Factory
*/
protected $auth;
/**
* Create a new middleware instance.
*
* @param \Illuminate\Contracts\Auth\Factory $auth
* @return void
*/
public function __construct(Auth $auth)
{
$this->auth = $auth;
}
...
}
這裡的$auth
其實是\Illuminate\Contracts\Auth\Factory
介面的一個例項,通過建構函式注入進來,通過dd($this->auth)
方式發現這個其實就是Illuminate\Auth\AuthManager
例項,它實現了Illuminate\Contracts\Auth\Factory
介面:
// \Illuminate\Contracts\Auth\Factory.php
namespace Illuminate\Contracts\Auth;
interface Factory
{
/**
* Get a guard instance by name.
*
* @param string|null $name
* @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
*/
public function guard($name = null);
/**
* Set the default guard the factory should serve.
*
* @param string $name
* @return void
*/
public function shouldUse($name);
}
這個介面有guard()
方法,所以上面可以直接鏈式呼叫。
通過介面定義的宣告,我們可以知道guard()
返回\Illuminate\Contracts\Auth\Guard
或者\Illuminate\Contracts\Auth\StatefulGuard
這兩個介面,具體在AuthManager
中的實現是這樣的:
// \Illuminate\Auth\AuthManager.php
/**
* Attempt to get the guard from the local cache.
*
* @param string|null $name
* @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
*/
public function guard($name = null)
{
$name = $name ?: $this->getDefaultDriver();
return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
}
通過我們在middleware()
中傳過來的引數建立對應的guard
例項,沒有就是預設driver
對應的guard
,最後check()
。
這節最後講一下
AuthManager
是什麼時候建立的?
Laravel
框架初始化時,很多服務都是以服務提供者(ServiceProvider
)的形式建立的,AuthManager
就是AuthServiceProvider
建立的:
// \Illuminate\Auth\AuthServiceProvider.php
namespace Illuminate\Auth;
class AuthServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->registerAuthenticator();
....
}
/**
* Register the authenticator services.
*
* @return void
*/
protected function registerAuthenticator()
{
$this->app->singleton('auth', function ($app) {
// Once the authentication service has actually been requested by the developer
// we will set a variable in the application indicating such. This helps us
// know that we need to set any queued cookies in the after event later.
$app['auth.loaded'] = true;
return new AuthManager($app);
});
....
}
....
}
AuthServiceProvider
中在註冊時呼叫registerAuthenticator()
,建立auth
單例指向AuthManager
例項。
通過上面的一波分析,我們知道guard
的建立是受AuthManager
管理的,AuthManager
在這裡的指責就是解析driver
並建立guard
。
所以現在整個middleware('auth')
的流程大致如下:
上面說到AuthManager
建立了guard
,然後呼叫check()
,我先現在來分析下Guard
。還是那句話,不管上層業務程式碼多麼複雜,底層的介面往往是很簡單的。Lumen/Laravel
框架中大部分介面被設計成是一種契約(Contracts)
,Guard
也一樣的,它的程式碼在\vendor\illuminate\contracts\Auth\Guard.php
檔案中,只有6個方法:
// \Illuminate\Contracts\Auth\Guard.php
namespace Illuminate\Contracts\Auth;
interface Guard
{
// 判斷當前使用者是否登入
public function check();
// 判斷當前使用者是否是遊客(未登入)
public function guest();
// 獲取當前認證的使用者
public function user();
// 獲取當前認證使用者的 id,嚴格來說不一定是 id,應該是這個模型中的主鍵欄位
public function id();
// 使用者驗證
public function validate(array $credentials = []);
// 設定當前認證過的使用者
public function setUser(Authenticatable $user);
}
很簡單,有木有~同樣,還有一個StatefulGuard
介面,繼承自Guard
介面並加了幾個有狀態的方法,代表有狀態,就是每次請求都帶有使用者的狀態資訊比如session
,程式碼如下:
// Illuminate\Contracts\Auth\StatefulGuard.php
namespace Illuminate\Contracts\Auth;
interface StatefulGuard extends Guard
{
// 指定資料驗證
public function attempt(array $credentials = [], $remember = false);
// 將這一次request驗證通過登入,不會儲存session/cookie
public function once(array $credentials = []);
// 登入
public function login(Authenticatable $user, $remember = false);
// 使用id登入
public function loginUsingId($id, $remember = false);
// 和once()一樣,不過是用id
public function onceUsingId($id);
// 通過remember cookie登入
public function viaRemember();
// 登出
public function logout();
}
UML
圖大致如下:
底層介面著實簡單,再來分析下上層的實現程式碼,框架中預設實現了幾個Guard
,比如Web開發用到的SessionGuard
,介面開發用到的TokenGuard
,這些都實現自\Illuminate\Contracts\Auth
或者\Illuminate\Contracts\Auth\StatefulGuard
,已經滿足我們日常所需了。
幾個Guard
的check()
方法都是一樣的,都定義在GuardHelpers
這個Trait
中:
// \Illuminate\Auth\GuardHelpers.php
/**
* Determine if the current user is authenticated.
*
* @return bool
*/
public function check()
{
return ! is_null($this->user());
}
user()
就是在不同的Guard
中實現了,後面也主要看這個方法。
什麼是Trait:
你可以理解成一系列方法的集合,就是把經常使用到的重複方法整合起來,在class裡面直接use使用,上下文還是引用它的那個class,減少了重複程式碼量,而且比class更輕量,不需要new在使用。
6.1 RequestGuard.php
RequestGuard
認證一個http
請求,具體怎麼認證,它是通過callback
實現的,認證邏輯在callback
中直接放到了上層讓使用者自定義,UML
圖:
看程式碼實現也很簡單:
// \Illuminate\Auth\RequestGuard.php
/**
* Get the currently authenticated user.
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{
// If we've already retrieved the user for the current request we can just
// return it back immediately. We do not want to fetch the user data on
// every call to this method because that would be tremendously slow.
if (! is_null($this->user)) {
return $this->user;
}
return $this->user = call_user_func(
$this->callback, $this->request, $this->getProvider()
);
}
RequestGuard
很多文章都是一筆帶過,這裡我說一下,通常我們使用不到RequestGuard
,只有在自定義Guard
時才用得上。
使用方式如下
AuthServiceProvider
中註冊自定義的guard
,設定名稱和callback
:
// App\Providers\AuthServiceProvider.php
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
/**
* Register any application authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Auth::viaRequest('custom-token', function ($request) {
return User::where('my-token', $request->my_token)->first();
});
}
auth.php
中配置自定義guard
'guards' => [
'my-api' => [
'driver' => 'custom-token',
],
],
- 使用
還是上面的例子:
Route::get('/user/profile', 'UserController@profile')->middleware('auth:my-api');
最後在認證的時候就會直接使用我們設定的callback
了。
上面viaRequest()
也是定義AuthManager
中:
// \Illuminate\Auth\AuthManager.php
/**
* Register a new callback based request guard.
*
* @param string $driver
* @param callable $callback
* @return $this
*/
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;
});
}
6.2 SessionGuard
見名知義,此guard
是基於session
的,一般最常用的就是這個了。由於是基於session
所以是有狀態的,所以這個類定義的時候實現了StatefulGuard
介面,而且加了更多邏輯程式碼和註釋加起來有800+行,
// \Illuminate\Auth\SessionGuard.php
namespace Illuminate\Auth;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Contracts\Auth\SupportsBasicAuth;
class SessionGuard implements StatefulGuard, SupportsBasicAuth
{
...
}
UML
圖:
使用者認證的程式碼稍微複雜一點,如下:
// \Illuminate\Auth\SessionGuard.php
/**
* Get the currently authenticated user.
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{
if ($this->loggedOut) {
return;
}
// If we've already retrieved the user for the current request we can just
// return it back immediately. We do not want to fetch the user data on
// every call to this method because that would be tremendously slow.
if (! is_null($this->user)) {
return $this->user;
}
$id = $this->session->get($this->getName());
// First we will try to load the user using the identifier in the session if
// one exists. Otherwise we will check for a "remember me" cookie in this
// request, and if one exists, attempt to retrieve the user using that.
if (! is_null($id) && $this->user = $this->provider->retrieveById($id)) {
$this->fireAuthenticatedEvent($this->user);
}
// If the user is null, but we decrypt a "recaller" cookie we can attempt to
// pull the user data on that cookie which serves as a remember cookie on
// the application. Once we have a user we can return it to the caller.
if (is_null($this->user) && ! is_null($recaller = $this->recaller())) {
$this->user = $this->userFromRecaller($recaller);
if ($this->user) {
$this->updateSession($this->user->getAuthIdentifier());
$this->fireLoginEvent($this->user, true);
}
}
return $this->user;
}
梳理下,大致是先從session
獲取使用者的主鍵id
,然後通過特定的UserProvider
查詢使用者,查詢成功說明驗證成功,如果沒有,就用recaller
查詢使用者,這裡就是remember token
查詢,就是登入時“記住我”的那個選項,remember token
是儲存在cookie
當中的,如果remember token
查詢成功,就說明驗證成功,否則驗證失敗。
6.3 TokenGuard
TokenGuard
也實現了Guard
介面,適用於無狀態的api
認證,UML
圖:
由於不要維護狀態整個程式碼就簡單很多:
// \Illuminate\Auth\TokenGuard.php
namespace Illuminate\Auth;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Http\Request;
class TokenGuard implements Guard
{
...
/**
* Get the currently authenticated user.
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{
// If we've already retrieved the user for the current request we can just
// return it back immediately. We do not want to fetch the user data on
// every call to this method because that would be tremendously slow.
if (! is_null($this->user)) {
return $this->user;
}
$user = null;
$token = $this->getTokenForRequest();
if (! empty($token)) {
$user = $this->provider->retrieveByCredentials([
$this->storageKey => $this->hash ? hash('sha256', $token) : $token,
]);
}
return $this->user = $user;
}
...
}
先從請求中獲取api_token
,再用api_token
從指定的UserProvider
查詢api_token
對應的使用者資訊。
至此,Laravel
中Guard
相關的分析已經差不多了,通過分析它的原始碼,我們深入瞭解了框架背後的思想,梳理的過程也是學習的過程,對新手而言能快速掌握guard
的相關知識並快速上手,對老鳥而言,我覺得這篇文章寫的已經很細了,能更好地瞭解框架背後的精髓寫出更優雅的程式碼。
在深入學習Guard
原始碼後,瞭解到底層歸納為兩個核心,一是UserProvider
,認證使用者資料來源,通常是本地資料庫,二是認證邏輯,邏輯這塊主要就是Guard
來做了。對於自定義Guard
,上面也稍微講了一點,通過AuthManager
的viaRequest
來做,對於使用者資料來源我們也不必拘泥於現有的,我們也可以將資料來源指向redis
或者遠端介面,只要實現相關介面,比如這樣:
namespace app\Providers;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
class RedisUserProvider implements UserProvider
{
/**
* Retrieve a user by their unique identifier.
*
* @param mixed $identifier
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifier)
{
// TODO: 通過id取redis中對應的使用者
}
....
}
也可以從遠端介面獲取:
class ApiUserProvider implements UserProvider
{
/**
* Retrieve a user by their unique identifier.
*
* @param mixed $identifier
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifier)
{
// TODO: 通過id構造curl請求結果
}
}
最後,附上一張我在學習過程中總結的UML
圖:
文章如果有誤,還請大家指正。
本作品採用《CC 協議》,轉載必須註明作者和本文連結