如何重寫laravel自帶的auth 原 薦

本文Laravel版本爲5.5

laravel中的實現方式

laravel自帶了完整的auth方案,可以參照文章laravel的應用。我這裏實現舉出幾個重要的步驟:

    //生成代碼
    php artisan make:auth
    //導入數據表結構
    php artisan migrate

重要的文件:

//用戶模型
app/Http/User.php

//業務邏輯
app/Http/Controllers/Auth/*.php

//配置文件
config/auth.php

我們觀察一下配置文件,個人的理解我加上註釋寫在後面:

<?php
return [
    'defaults' => [                      /*默認配置*/ 
        'guard' => 'web',                /*guard(我個人看成檢驗器,翻譯是守衛、監視者) 使用web的配置 */
        'passwords' => 'users',           /*密碼管理,使用users的配置*/
    ],
    'guards' => [                                  
        'web' => [                     /*web的檢驗器的配置,有驅動器和提供者2個屬性*/
            'driver' => 'session',
            'provider' => 'users',
        ],
        'api' => [                      /*api的檢驗器的配置,有驅動器和提供者2個屬性*/
            'driver' => 'token',
            'provider' => 'users',
        ],
    ],
    'providers' => [                     /*提供者的驅動器和模型*/
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Http\User::class,
        ]
    ],
    'passwords' => [
        'users' => [                    /*密碼管理的提供者屬性*/
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
        ],
    ]
];

從配置來看,我們可以把laravel的auth看成多個部分(而passwords應該屬於其中一項功能):

  1. 入口(web|api|......)
  2. 驗證器guards
  3. 提供者providers

分析源碼

以登錄爲例,我們開始進入源碼閱讀,當我們點進去看LoginController.php時,發現他沒有幾行代碼。但是他用了一個trait,至於什麼是trait,請參照php的trait。裏面也只是些簡單的業務邏輯實現方法,如果不需要,可以重寫,或者註釋。

但是,我從構造函數發現了,他使用了middleware,如果你不懂什麼叫middleware,請參照我個人的bloglaravel(5.5)自定義middle ware

$this->middleware('guest')->except('logout');

很明顯,他使用了中間件進行處理,我們進行代碼追蹤,就可以進入下一步了。

laravel是如何注入guard

guest這個中間件,定義在app/Http/Kernel.php中。

'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class

主要的代碼如下:

 public function handle($request, Closure $next, $guard = null)
    {
        if (Auth::guard($guard)->check()) {
            return redirect('/');
        }

        return $next($request);
    }

那,我們找到了我們要找的Auth。Auth是一個門面,也就是一個快捷使用的類的入口,我們找到他的實現類的位置\Illuminate\Auth\AuthServiceProvider,終於算是找到我們的正主了。

    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);
        });

        $this->app->singleton('auth.driver', function ($app) {
            return $app['auth']->guard();
        });
    }

每當請求進入的時候,只要使用了Auth這個門面的地方,都會註冊這個驗證身份。實現類,就是AuthManager類。在這個類中,有這麼一個方法,將驗證器注入。

    public function guard($name = null)
    {
        $name = $name ?: $this->getDefaultDriver();
        return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
    }

發現了嘛?driver就是從這裏寫入的,配合配置文件中的driver就會生成一個driver類。不過這個代碼質量也不是很高,比如下面的:

  $driverMethod = 'create'.ucfirst($config['driver']).'Driver';

emmm,硬生生的字符串拼接獲取方法名???好把,這裏按照laravel這麼“優雅的框架”,不應該加上工廠模式嘛?額,小小吐槽一次。

laravel的驗證器是如何工作的

接下來,我們就可以看他的驗證器是如何實現的了。以TokenGuard爲例把。

   public function createTokenDriver($name, $config)
    {

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

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

        return $guard;
    }

返回了一個TokeGuard的類,細看下去,裏面無非是判斷有沒有值提交[api_key|api_token],沒有的話就去找http的驗證頭Authorization Bearer或者$_SERVER['PHP_AUTH_PW'],然後,重要的地方來了。

    public function validate(array $credentials = [])
    {
        if (empty($credentials[$this->inputKey])) {
            return false;
        }
        $credentials = [$this->storageKey => $credentials[$this->inputKey]];
        if ($this->provider->retrieveByCredentials($credentials)) {
            return true;
        }
        return false;
    }

這裏直接查表?條件就是key= input key的模式,實在太恐怖了,查表至少也要用主鍵把?

修改重寫

從上面的分析來看,大多數的功能代碼已經分析完畢了,我們開始着手重寫這個複雜的輪子把。重寫,當然是先實現底層類比較好。

model的重寫

當一個用戶的屬性來源於多張表的時候,一個模型肯定是不夠了,但是laravel的Auth,顯然是不方便拓展的。因此,我們只能改寫一部分。 我們找到laravel實現的地方!回想開始的配置,默認使用的驅動器是eloquent,類是App\Http\User。 找到實現類Illuminate\Auth\uentUserProvider。裏面實現了多種查詢方式,那我們發現了一個規律,查詢之前,他一定是先創建模型,模型就是我們的類App\Http\User。 比如:

 $query = $this->createModel()->newQuery();
        foreach ($credentials as $key => $value) {
            if (! Str::contains($key, 'password')) {
                $query->where($key, $value);
            }
        }

return $query->first();

找了很久,我發現,我們不好改寫這個類,因爲這個類並沒有用配置去注入,但是,我找到了一條規律,查詢之前,必然會先newQuery(),這個方法的調用對象是我們配置的。 於是,我的改寫版出路,我改寫了App\Http\User的newQuery()

    public function  newQuery() {
                $query = $this->newBaseQueryBuilder();
                $builder =  new class($query) extends Builder {

                        /**
                         * Create a new Eloquent query builder instance.
                         *
                         * @param  \Illuminate\Database\Query\Builder  $query
                         * @return void
                         */
                        public function __construct(QueryBuilder $query)
                        {

                                parent::__construct($query);
                        }
                        /**
                         * Execute the query and get the first result.
                         *
                         * @param  array  $columns
                         * @return \Illuminate\Database\Eloquent\Model|object|static|null
                         */
                        public function first($columns = ['*']) {
                                /** @var Model $user */
                                $user =  $this->take(1)->get($columns)->first();
                                //從別的模型查數據  設置到這個模型裏面
                                if($user!=null) {
                                        //拓展user的其他屬性
                                        $user->setAttribute('acl', ['dsadas', 'dsadas', 'dsada']);
                                }
                                //var_dump($user);exit();
                                return $user;
                        }

                };
                $builder->setModel($this);
                return $builder;
        }

是不是,用戶信息存在於多表的問題就這麼簡單的解決了。

guard的重寫

但是,比如上面的接口,一般調用會很勤快,這個時候,我們還每次用這麼亂的資料去查庫,是不是會導致db卡IO(併發的時候)。一般情況,我們已經知道了用戶的ID,接口一般都會附帶用戶信息進行請求,不是麼?那麼,我們很顯然,只需要根據ID進行查詢用戶信息即可。

首先,我們新建類App\Library\Auth\TokenGuard以備接下來使用。

再看之前的TokeGuard類,我們發現他的條件是使用的token進行查詢的,我們不妨把他改成主鍵,但是主鍵從何而來?所以我們要先獲取到user_id。我設想,我的token是通過user_id加密而來,這時候,我只需要解密就好了。

    $this->uid = $this->_validateToken($token);  //僞代碼
    public function validate(array $credentials = [])
    {
        if($this->provider->retrieveById($this->uid)) {  //直接改成從ID查詢
            return true;
        }
        return false;
    }

顯然,既然我們已經知道ID,用ID查詢豈不美滋滋,如果對性能要求特別高,而且不想在數據庫進行查詢,完全可以把這個部分實現在redis,這裏不是我們的重點,我就不細講了(實現還是修改newQuery修改成redis查詢即可,記得返回Model對象即可)。

AuthManager的重寫

既然驗證器也寫好了,我們肯定要把自己的驗證器覆蓋。 新建一個類,重寫createTokenDriver方法用自己的類覆蓋即可

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

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

                return $guard;
        }
}

其他配置

最後,我們需要把我們的認證服務加載到內核中來。 重寫App\Providers\AuthServiceProvider集成原本的認證類,只需重寫registerAuthenticator,其它的功能就照樣可以用。

class AuthServiceProvider extends \Illuminate\Auth\AuthServiceProvider
{
        protected function registerAuthenticator()
        {
                $this->app->singleton('auth', function ($app) {
                        $app['auth.loaded'] = true;

                        return new \App\Library\Auth\AuthManager($app);
                });

                $this->app->singleton('auth.driver', function ($app) {
                        return $app['auth']->guard();
                });
        }
}

config/app.php 修改

Illuminate\Auth\AuthServiceProvider::class
=》
App\Providers\AuthServiceProvider::class

總結

個人心得

其實個人不太喜歡這種方式,畢竟登錄註冊這種含有業務邏輯的東西,我覺得封裝到框架內部的意義並不大。但是爲了偷懶的話,改寫複用也是一種方式。

備註

注意,修改密碼和更改更新之類的,需要在update之前unset那麼其它表的數據,才能進行更新,否則會報字段不存在。

源碼位置

一個正在實現的教材管理系統,替俺一個關係很好的老師做的edu_book。 還有公司的一個項目也共用這種方法重寫auth,就不能開源了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章