Laravel 框架擴充套件 Auth 認證,實現自定義 driver,guard

insss發表於2018-06-20

原文連結:https://fordawn.com/article/36


最近研究 laravel 自帶的認證,發現使用token認證方式的話,預設傳值欄位和資料庫中欄位名都是api_token,這讓我很不爽,laravel 這樣的框架一定有什麼自定義的途徑,網上查詢無果,那就自己看看原始碼吧。

我們知道,認證其實就是呼叫Auth::guard()來執行的,那我們不妨從這裡開始看起。

自定義dirver配置項

我們知道AuthIlluminate/Auth/AuthManager.php的alias,那我們開啟這個檔案,找到guard後,發現判斷用實際哪個guard的程式碼在方法resolve中,其中有一段程式碼:

if (isset($this->customCreators[$config['driver']])) {
    return $this->callCustomCreator($name, $config);
}

發現這個類中有一個陣列customCreators,這說明,我們是可以自定義driver的。也就是說,雖然config/auth.phpguards中說driver只支援sessiontoken,但實際上是可以自己擴充套件的。

那怎麼擴充套件呢,順藤摸瓜,發現class AuthManager中有一個方法extend

public function extend($driver, Closure $callback)
{
    $this->customCreators[$driver] = $callback;

    return $this;
}

這個方法就是給Auth新增自定義driver用的,那我們直接找個provider擴充套件一下Auth,把自己的driver新增進去就好。

我們可以先看一下預設的token driver是怎麼寫的:

public function createTokenDriver($name, $config)
{
    $guard = new TokenGuard(
        $this->createUserProvider($config['provider'] ?? null),
        $this->app['request']
    );

    $this->app->refresh('request', $guard, 'setRequest');

    return $guard;
}

就這麼幾行程式碼,在5.6以後TokenGuard的建構函式是有4個引數的,原型如下

public function __construct(UserProvider $provider, Request $request, $inputKey = 'api_token', $storageKey = 'api_token');

也就是說,我們把後兩個引數傳進去就可以了。那就開始搞起,例如在AppServiceProviderboot方法中新增:

Auth::extend('web-token', function ($app, $name, $config) {
    $guard = new WebTokenGuard(
        $this->createUserProvider($config['provider'] ?? null),
        $app['request'],
        'token',
        'token'
    );

    $app->refresh('request', $guard, 'setRequest');

    return $guard;
});

這樣我們就有了一個新的driver——web-token,config中我們就可以寫:

'guards' => [
    'web-api' => [
        'driver' => 'web-token',
        'provider' => 'tokens',
    ],
],

這裡發現我的provider也不是users,是因為我新建了一張表專門存token用,配置起來很方便,在同檔案的providers中新增如下配置即可:

'tokens' => [
    'driver' => 'eloquent',
    'model' => App\Models\Token::class,
],

但是這樣一來,就產生了新的問題:我們在使用laravel的時候,總會使用類似Auth::id()Auth::user()這樣的方法,因為確實很方便,而我們的認證現在雖然沒有問題了,但是因為跟user不是同表,這些方法現在不能用了,那怎麼辦呢?我們首先來解決第一個問題:

實現 Auth:id()

我們可以看一下框架帶的User Model,發現其繼承自Illuminate\Foundation\Auth\User,順藤摸瓜,我們發現他實現了三個介面:

use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

這三個介面各有作用,比方說第一個AuthenticatableContract,就是我們目前要實現的,因為其中有一個方法public function getAuthIdentifier();,這個方法就是執行Auth::id()時候會呼叫的。

第二個介面是授權時候用的,第三個是重置密碼時候用的,這兩個我們暫時不需要,那就等以後用到時候再說。

我們現在讓Token Model實現第一個介面,方法的實現根據各自的情況有所不同,例如我的token表中,使用者的id列名為user_id,那我的方法就是這樣子的:

public function getAuthIdentifierName()
{
    return 'user_id';
}

而因為我實現了實體關係:

public function user()
{
    return $this->belongsTo('App\User');
}

其他的一些方法可以這樣寫:

public function getAuthPassword()
{
    return $this->user->password;
}

這樣大體上第一個問題我們就解決了,接下來解決第二個問題。

實現 Auth:user()

首先,我們來找一下這個user()方法在哪裡,看了AuthManager以後發現,這個user()原來是呼叫的Guard中的方法,那怎麼辦呢?我們擴充套件一下TokenGuard吧。

新建一個php類繼承於TokenGuard,並重寫user()方法:

class WebTokenGuard extends TokenGuard
{
    public function user()
    {
        $token = parent::user();

        if (!$token) {
            return null;
        }

        return $token->user;
    }
}

return null並不會產生問題,這樣會使Guard的check()方法返回false,丟擲AuthenticationException異常,這與正常的邏輯是一致的。

這樣我們就解決了Auth::user()的問題,擴充套件類還可以解決一些其他的問題,例如在5.4版本中,$inputKey$storageKey是直接寫在建構函式中的,而不是引數穿進去的,擴充套件類也可以解決這個問題。

最佳化

因為使用用了新的Guard類,我們在provider中的程式碼也需要做一些修改,而同時,我們把欄位名也寫到config中,便於以後修改,最佳化後的程式碼如下:

# auth.config
'web-api' => [
    'driver' => 'web-token',
    'input_key' => 'token',
    'storage_key' => 'token',
    'provider' => 'tokens',
],

# AppServiceProvider 的 boot
Auth::extend('web-token', function ($app, $name, $config) {
    $guard = new WebTokenGuard(
        $this->createUserProvider($config['provider'] ?? null),
        $app['request'],
        $config['input_key'],
        $config['storage_key']
    );

    $app->refresh('request', $guard, 'setRequest');

    return $guard;
});

這樣基本就趨近於最終版本了。

laravel 確實是一個龐大的框架,這次只涉及到了認證的問題,而未談及授權,例如Auth::can()這樣的方法,現在也是不能使用的。

授權也是一個大頭的問題,不同的需求採用的方式不盡相同,laravel也自帶一套授權的機制,希望本篇文章可以達到一個拋磚引玉的作用,給小夥伴們一些啟發。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章