Laravel核心程式碼學習 -- 擴充套件使用者認證系統

kevinyan發表於2018-07-09

擴充套件使用者認證系統

上一節我們介紹了Laravel Auth系統實現的一些細節知道了Laravel是如何應用看守器和使用者提供器來進行使用者認證的,但是針對我們自己開發的專案或多或少地我們都會需要在自帶的看守器和使用者提供器基礎之上做一些定製化來適應專案,本節我會列舉一個在做專案時遇到的具體案例,在這個案例中用自定義的看守器和使用者提供器來擴充套件了Laravel的使用者認證系統讓它能更適用於我們自己開發的專案。

在介紹使用者認證系統基礎的時候提到過Laravel自帶的註冊和登入驗證使用者密碼時都是去驗證採用bcypt加密儲存的密碼,但是很多已經存在的老系統中使用者密碼都是用鹽值加明文密碼做雜湊後儲存的,如果想要在這種老系統中應用Laravel開發專案的話那麼我們就不能夠再使用Laravel自帶的登入和註冊方法了,下面我們就通過例項看看應該如何擴充套件Laravel的使用者認證系統讓它能夠滿足我們專案的認證需求。

修改使用者註冊

首先我們將使用者註冊時,使用者密碼的加密儲存的方式由bcypt加密後儲存改為由鹽值與明文密碼做雜湊後再儲存的方式。這個非常簡單,上一節已經說過Laravel自帶的使用者註冊方法是怎麼實現了,這裡我們直接將\App\Http\Controllers\Auth\RegisterController中的create方法修改為如下:

/**
 * Create a new user instance after a valid registration.
 *
 * @param  array  $data
 * @return User
 */
protected function create(array $data)
{
    $salt = Str::random(6);
    return User::create([
        'email' => $data['email'],
        'password' => sha1($salt . $data['password']),
        'register_time' => time(),
        'register_ip' => ip2long(request()->ip()),
        'salt' => $salt
    ]);
}
複製程式碼

上面改完後註冊使用者後就能按照我們指定的方式來儲存使用者資料了,還有其他一些需要的與使用者資訊相關的欄位也需要儲存到使用者表中去這裡就不再贅述了。

修改使用者登入

上節分析Laravel預設登入的實現細節時有說登入認證的邏輯是通過SessionGuardattempt方法來實現的,在attempt方法中SessionGuard通過EloquentUserProviderretriveBycredentials方法從使用者表中查詢出使用者資料,通過 validateCredentials方法來驗證給定的使用者認證資料與從使用者表中查詢出來的使用者資料是否吻合。

下面直接給出之前講這塊時引用的程式碼塊:

class SessionGuard implements StatefulGuard, SupportsBasicAuth
{
    public function attempt(array $credentials = [], $remember = false)
    {
        $this->fireAttemptEvent($credentials, $remember);

        $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
	   //如果登入認證通過,通過login方法將使用者物件裝載到應用裡去
        if ($this->hasValidCredentials($user, $credentials)) {
            $this->login($user, $remember);

            return true;
        }
        //登入失敗的話,可以觸發事件通知使用者有可疑的登入嘗試(需要自己定義listener來實現)
        $this->fireFailedEvent($user, $credentials);

        return false;
    }
    
    protected function hasValidCredentials($user, $credentials)
    {
        return ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
    }
}

class EloquentUserProvider implements UserProvider
{
    從資料庫中取出使用者例項
    public function retrieveByCredentials(array $credentials)
    {
        if (empty($credentials) ||
           (count($credentials) === 1 &&
            array_key_exists('password', $credentials))) {
            return;
        }

        $query = $this->createModel()->newQuery();

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

        return $query->first();
    }
    
    //通過給定使用者認證資料來驗證使用者
    /**
     * Validate a user against the given credentials.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  array  $credentials
     * @return bool
     */
    public function validateCredentials(UserContract $user, array $credentials)
    {
        $plain = $credentials['password'];

        return $this->hasher->check($plain, $user->getAuthPassword());
    }
}
複製程式碼

自定義使用者提供器

好了, 看到這裡就很明顯了, 我們需要改成自己的密碼驗證就是自己實現一下validateCredentials就可以了, 修改$this->hasher->check為我們自己的密碼驗證規則。

首先我們來重寫$user->getAuthPassword(); 在User模型中覆蓋其從父類中繼承來的這個方法,把資料庫中使用者表的saltpassword傳遞到validateCredentials中來:

class user extends Authenticatable
{
    /**
     * 覆蓋Laravel中預設的getAuthPassword方法, 返回使用者的password和salt欄位
     * @return array
     */
    public function getAuthPassword()
    {
        return ['password' => $this->attributes['password'], 'salt' => $this->attributes['salt']];
    }
}    
複製程式碼

然後我們用一個自定義的使用者提供器,通過它的validateCredentials來實現我們自己系統的密碼驗證規則,由於使用者提供器的其它方法不用改變沿用EloquentUserProvider裡的實現就可以,所以我們讓自定義的使用者提供器繼承自EloquentUserProvider

namespace App\Foundation\Auth;

use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Str;

class CustomEloquentUserProvider extends EloquentUserProvider
{

    /**
     * Validate a user against the given credentials.
     *
     * @param \Illuminate\Contracts\Auth\Authenticatable $user
     * @param array $credentials
     */
    public function validateCredentials(Authenticatable $user, array $credentials)
    {
        $plain = $credentials['password'];
        $authPassword = $user->getAuthPassword();

        return sha1($authPassword['salt'] . $plain) == $authPassword['password'];
    }
}
複製程式碼

接下來通過Auth::provider()CustomEloquentUserProvider註冊到Laravel系統中,Auth::provider方法將一個返回使用者提供器物件的閉包作為使用者提供器建立器以給定名稱註冊到Laravel中,程式碼如下:

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        \Auth::provider('custom-eloquent', function ($app, $config) {
            return New \App\Foundation\Auth\CustomEloquentUserProvider($app['hash'], $config['model']);
        });
    }
    ......
}
複製程式碼

註冊完使用者提供器後我們就可以在config/auth.php裡配置讓看守器使用新註冊的custom-eloquent作為使用者提供器了:

//config/auth.php
'providers' => [
    'users' => [
        'driver' => 'coustom-eloquent',
        'model' => \App\User::class,
    ]
]
複製程式碼

自定義認證看守器

好了,現在密碼認證已經修改過來了,現在使用者認證使用的看守器還是SessionGuard, 在系統中會有對外提供API的模組,在這種情形下我們一般希望使用者登入認證後會返回給客戶端一個JSON WEB TOKEN,每次呼叫介面時候通過這個token來認證請求介面的是否是有效使用者,這個需求需要我們通過自定義的Guard擴充套件功能來完成,有個composer"tymon/jwt-auth": "dev-develop", 他的1.0beta版本帶的JwtGuard是一個實現了Illuminate\Contracts\Auth\Guard的看守器完全符合我上面說的要求,所以我們就通過Auth::extend()方法將JwtGuard註冊到系統中去:

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        \Auth::provider('custom-eloquent', function ($app, $config) {
            return New \App\Foundation\Auth\CustomEloquentUserProvider($app['hash'], $config['model']);
        });
        
        \Auth::extend('jwt', function ($app, $name, array $config) {
            // 返回一個 Illuminate\Contracts\Auth\Guard 例項...
            return new \Tymon\JWTAuth\JwtGuard(\Auth::createUserProvider($config['provider']));
        });
    }
    ......
}
複製程式碼

定義完之後,將 auth.php 配置檔案的guards配置修改如下:

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'jwt', // token ==> jwt
        'provider' => 'users',
    ],
],
複製程式碼

接下來我們定義一個API使用的登入認證方法, 在認證中會使用上面註冊的jwt看守器來完成認證,認證完成後會返回一個JSON WEB TOKEN給客戶端

Route::post('apilogin', 'Auth\LoginController@apiLogin');
複製程式碼
class LoginController extends Controller
{
   public function apiLogin(Request $request)
   {
        ...
	
        if ($token = $this->guard('api')->attempt($credentials)) {
            $return['status_code'] = 200;
            $return['message'] = '登入成功';
            $response = \Response::json($return);
            $response->headers->set('Authorization', 'Bearer '. $token);
            return $response;
        }

    ...
    }
}
複製程式碼

總結

通過上面的例子我們講解了如何通過自定義認證看守器和使用者提供器擴充套件Laravel的使用者認證系統,目的是讓大家對Laravel的使用者認證系統有一個更好的理解知道在Laravel系統預設自帶的使用者認證方式無法滿足我們的需求時如何通過自定義這兩個元件來擴充套件功能完成我們專案自己的認證需求。

本文已經收錄在系列文章Laravel原始碼學習裡,歡迎訪問閱讀。

相關文章