Laravel核心代碼學習--用戶認證系統的實現細節

用戶認證系統的實現細節

上一節咱們介紹了Laravel Auth系統的基礎知識,說了他的核心組件都有哪些構成,這一節咱們會專一Laravel Auth系統的實現細節,主要關注Auth也就是AuthManager是如何裝載認證用的看守器(Guard)和用戶提供器(UserProvider)以及默認的用戶註冊和登陸的實現細節,經過梳理這些實現細節咱們也就能知道應該如何定製Auth認證來知足咱們本身項目中用戶認證的需求的。php

經過AuthManager裝載看守器和用戶提供器

AuthManager裝載看守器和用戶提供器用到的方法比較多,用文字描述不太清楚,咱們經過註解這個過程當中用到的方法來看具體的實現細節。git

namespace Illuminate\Auth;
class AuthManager implements FactoryContract
{
    /**
     * 嘗試從$guards屬性中獲取指定的Guard
     *
     * @param  string  $name
     * @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
     */
    public function guard($name = null)
    {
        $name = $name ?: $this->getDefaultDriver();

        return isset($this->guards[$name])
                    ? $this->guards[$name]
                    : $this->guards[$name] = $this->resolve($name);
    }
    
    /**
     * 解析出給定name的Guard
     *
     * @param  string  $name
     * @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
     *
     * @throws \InvalidArgumentException
     */
    protected function resolve($name)
    {
        //獲取Guard的配置
        //$config = ['driver' => 'session', 'provider' => 'users']
        $config = $this->getConfig($name);

        if (is_null($config)) {
            throw new InvalidArgumentException("Auth guard [{$name}] is not defined.");
        }
	   //若是經過extend方法爲guard定義了驅動器,這裏去調用自定義的Guard驅動器
        if (isset($this->customCreators[$config['driver']])) {
            return $this->callCustomCreator($name, $config);
        }
        //Laravel auth默認的配置這裏是執行createSessionDriver
        $driverMethod = 'create'.ucfirst($config['driver']).'Driver';

        if (method_exists($this, $driverMethod)) {
            return $this->{$driverMethod}($name, $config);
        }

        throw new InvalidArgumentException("Auth guard driver [{$name}] is not defined.");
    }
    
    /**
     * 從config/auth.php中獲取給定名稱的Guard的配置
     *
     * @param  string  $name
     * @return array
     */
    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'token',
            'provider' => 'users',
        ],
    ],
    protected function getConfig($name)
    {
        //'guards' => [
        //    'web' => [
        //        'driver' => 'session',
        //        'provider' => 'users',
        //    ],

        //    'api' => [
        //        'driver' => 'token',
        //        'provider' => 'users',
        //    ],
        //],
        // 根據Laravel默認的auth配置, 這個方法會獲取key "web"對應的數組
        return $this->app['config']["auth.guards.{$name}"];
    }
    
    /**
     * 調用自定義的Guard驅動器
     *
     * @param  string  $name
     * @param  array  $config
     * @return mixed
     */
    protected function callCustomCreator($name, array $config)
    {
        return $this->customCreators[$config['driver']]($this->app, $name, $config);
    }
    
    /**
     * 註冊一個自定義的閉包Guard 驅動器 到customCreators屬性中
     *
     * @param  string  $driver
     * @param  \Closure  $callback
     * @return $this
     */
    public function extend($driver, Closure $callback)
    {
        $this->customCreators[$driver] = $callback;

        return $this;
    }
    
    /**
     * 註冊一個自定義的用戶提供器建立器到 customProviderCreators屬性中
     *
     * @param  string  $name
     * @param  \Closure  $callback
     * @return $this
     */
    public function provider($name, Closure $callback)
    {
        $this->customProviderCreators[$name] = $callback;

        return $this;
    }
    
    /**
     * 建立基於session的認證看守器 SessionGuard
     *
     * @param  string  $name
     * @param  array  $config
     * @return \Illuminate\Auth\SessionGuard
     */
    public function createSessionDriver($name, $config)
    {
    	//$config['provider'] == 'users'
        $provider = $this->createUserProvider($config['provider'] ?? null);

        $guard = new SessionGuard($name, $provider, $this->app['session.store']);

        if (method_exists($guard, 'setCookieJar')) {
            $guard->setCookieJar($this->app['cookie']);
        }

        if (method_exists($guard, 'setDispatcher')) {
            $guard->setDispatcher($this->app['events']);
        }

        if (method_exists($guard, 'setRequest')) {
            $guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
        }

        return $guard;
    }
    
    //建立Guard驅動依賴的用戶提供器對象
    public function createUserProvider($provider = null)
    {
        if (is_null($config = $this->getProviderConfiguration($provider))) {
            return;
        }
        //若是經過Auth::provider方法註冊了自定義的用戶提供器creator閉包則去調用閉包獲取用戶提供器對象
        if (isset($this->customProviderCreators[$driver = ($config['driver'] ?? null)])) {
            return call_user_func(
                $this->customProviderCreators[$driver], $this->app, $config
            );
        }

        switch ($driver) {
            case 'database':
                return $this->createDatabaseProvider($config);
            case 'eloquent':
                //經過默認的auth配置這裏會返回EloquentUserProvider對象,它實現了Illuminate\Contracts\Auth 接口
                return $this->createEloquentProvider($config);
            default:
                throw new InvalidArgumentException(
                    "Authentication user provider [{$driver}] is not defined."
                );
        }
    }
    
    /**
     * 會經過__call去動態地調用AuthManager代理的Guard的用戶認證相關方法
     * 根據默認配置,這裏__call會去調用SessionGuard裏的方法
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        return $this->guard()->{$method}(...$parameters);
    }
}


複製代碼

註冊用戶

Laravel Auth系統中默認的註冊路由以下:github

$this->post('register', 'Auth\RegisterController@register');
複製代碼

因此用戶註冊的邏輯是由RegisterController的register方法來完成的web

class RegisterController extends Controller
{
    //方法定義在Illuminate\Foundation\Auth\RegisterUsers中
    public function register(Request $request)
    {
        $this->validator($request->all())->validate();

        event(new Registered($user = $this->create($request->all())));

        $this->guard()->login($user);

        return $this->registered($request, $user)
        
     }
     
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => 'required|string|min:6|confirmed',
        ]);
    }
    
    protected function create(array $data)
    {
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => bcrypt($data['password']),
        ]);
    }
         
}
複製代碼

register的流程很簡單,就是驗證用戶輸入的數據沒問題後將這些數據寫入數據庫生成用戶,其中密碼加密採用的是bcrypt算法,若是你須要改爲經常使用的salt加密碼明文作哈希的密碼加密方法能夠在create方法中對這部分邏輯進行更改,註冊完用戶後會調用SessionGuard的login方法把用戶數據裝載到應用中,注意這個login方法沒有登陸認證,只是把認證後的用戶裝載到應用中這樣在應用裏任何地方咱們都可以經過Auth::user()來獲取用戶數據啦。算法

用戶登陸認證

Laravel Auth系統的登陸路由以下數據庫

$this->post('login', 'Auth\LoginController@login');
複製代碼

咱們看一下LoginController裏的登陸邏輯api

class LoginController extends Controller
{
    /**
     * 處理登陸請求
     */
    public function login(Request $request)
    {
        //驗證登陸字段
        $this->validateLogin($request);
        //防止惡意的屢次登陸嘗試
        if ($this->hasTooManyLoginAttempts($request)) {
            $this->fireLockoutEvent($request);

            return $this->sendLockoutResponse($request);
        }
        //進行登陸認證
        if ($this->attemptLogin($request)) {
            return $this->sendLoginResponse($request);
        }

        $this->incrementLoginAttempts($request);

        return $this->sendFailedLoginResponse($request);
    }
    
    //嘗試進行登陸認證
    protected function attemptLogin(Request $request)
    {
        return $this->guard()->attempt(
            $this->credentials($request), $request->filled('remember')
        );
    }
    
    //獲取登陸用的字段值
    protected function credentials(Request $request)
    {
        return $request->only($this->username(), 'password');
    }
}
複製代碼

能夠看到,登陸認證的邏輯是經過SessionGuardattempt方法來實現的,其實就是Auth::attempt(), 下面咱們來看看attempt方法裏的邏輯:數組

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);
    }
}
複製代碼

SessionGuardattempt方法首先經過用戶提供器的retriveBycredentials方法經過用戶名從用戶表中查詢出用戶數據,認證用戶信息是經過用戶提供器的validateCredentials來實現的,全部用戶提供器的實現類都會實現UserProvider契約(interface)中定義的方法,經過上面的分析咱們知道默認的用戶提供器是EloquentUserProviderbash

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();
    }
    
    //經過給定用戶認證數據來驗證用戶
    public function validateCredentials(UserContract $user, array $credentials)
    {
        $plain = $credentials['password'];

        return $this->hasher->check($plain, $user->getAuthPassword());
    }
}

class BcryptHasher implements HasherContract
{
    //經過bcrypt算法計算給定value的散列值
    public function make($value, array $options = [])
    {
        $hash = password_hash($value, PASSWORD_BCRYPT, [
            'cost' => $this->cost($options),
        ]);

        if ($hash === false) {
            throw new RuntimeException('Bcrypt hashing not supported.');
        }

        return $hash;
    }
    
    //驗證散列值是否給定明文值經過bcrypt算法計算獲得的
    public function check($value, $hashedValue, array $options = [])
    {
        if (strlen($hashedValue) === 0) {
            return false;
        }

        return password_verify($value, $hashedValue);
    }
}
複製代碼

用戶密碼的驗證是經過EloquentUserProvider依賴的hasher哈希器來完成的,Laravel認證系統默認採用bcrypt算法來加密用戶提供的明文密碼而後存儲到用戶表裏的,驗證時haser哈希器的check方法會經過PHP內建方法password_verify來驗證實文密碼是不是存儲的密文密碼的原值。cookie

用戶認證系統的主要細節梳理完後咱們就知道如何定義咱們本身的看守器(Guard)或用戶提供器(UserProvider)了,首先他們必須實現各自遵照的契約裏的方法纔可以無縫接入到Laravel的Auth系統中,而後還須要將本身定義的Guard或Provider經過Auth::extendAuth::provider方法註冊返回Guard或者Provider實例的閉包到Laravel中去,Guard和UserProvider的自定義不是必須成套的,咱們能夠單獨自定義Guard仍使用默認的EloquentUserProvider,或者讓默認的SessionGuard使用自定義的UserProvider。

下一節我會給出一個咱們之前項目開發中用到的一個案例來更好地講解應該如何對Laravel Auth系統進行擴展。

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

相關文章
相關標籤/搜索