Laravel 登录原理剖析

简介

Laravel 中实现用户认证非常简单。实际上,几乎所有东西都已经为你配置好了。其配置文件位于 config/auth.php,其中包含了用于调整认证服务行为的注释清晰的选项配置。

其核心是由 Laravel 的认证组件的「看守器」和「提供器」组成。看守器定义了该如何认证每个请求中用户。例如,Laravel 自带的 session 看守器会使用 session 存储和 cookies 来维护状态。

提供器中定义了该如何从持久化的存储数据中检索用户。Laravel 自带支持使用 Eloquent 和数据库查询构造器来检索用户。当然,你可以根据需要自定义其他提供器。

不过对大多数应用而言,可能永远都不需要修改默认身份认证配置。

上面的简介出自 Laravel-China 社区的文档中。

除此之外,Laravel还提供了一个简单的命令来快速生成身份验证所需的路由和视图:

php artisan make:auth

原理剖析

Auth::routes()

make:auth命令在routes/web.php中插入了下面的代码:

Auth::routes();

其中,Auth 是使用 Facades 来调用的。那么我们可以在文件vendor/laravel/framework/src/Illuminate/Support/Facades/Auth.php中找到routes方法。

// Laravel5.8 中的 routes 方法
public static function routes(array $options = [])
{
    static::$app->make('router')->auth($options);
}

// Laravel5.5 中的 routes 方法
public static function routes()
{
    static::$app->make('router')->auth();
}

在这个方法中,调用了vendor/laravel/framework/src/Illuminate/Routing/Router.php文件中的 auth 方法。

// Laravel5.8 中的 auth 方法
public function auth(array $options = [])
{
    // Authentication Routes...
    $this->get('login', 'Auth\LoginController@showLoginForm')->name('login');
    $this->post('login', 'Auth\LoginController@login');
    $this->post('logout', 'Auth\LoginController@logout')->name('logout');

    // Registration Routes...
    if ($options['register'] ?? true) {
        $this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
        $this->post('register', 'Auth\RegisterController@register');
    }

    // Password Reset Routes...
    if ($options['reset'] ?? true) {
        $this->resetPassword();
    }

    // Email Verification Routes...
    if ($options['verify'] ?? false) {
        $this->emailVerification();
    }
}

// Laravel 5.5 中的 auth 方法
public function auth()
{
    // Authentication Routes...
    $this->get('login', 'Auth\LoginController@showLoginForm')->name('login');
    $this->post('login', 'Auth\LoginController@login');
    $this->post('logout', 'Auth\LoginController@logout')->name('logout');

    // Registration Routes...
    $this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
    $this->post('register', 'Auth\RegisterController@register');

    // Password Reset Routes...
    $this->get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
    $this->post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
    $this->get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
    $this->post('password/reset', 'Auth\ResetPasswordController@reset');
}

从上面的代码中我们可以看到,auth 方法为我们注册了登录、登出、密码重设等路由。而在 Laravel5.8 中我们则可以在Auth::routes()中传入参数来控制个别路由是否注册,比如:

Auth::routes([
    'register' => false, 
    'reset' => false, 
    'verify' => true
]);

可以禁用注册和重置路由,启用邮箱验证路由。

登录原理

LoginController中引入了Illuminate\Foundation\Auth\AuthenticatesUsers这个 trait。登录的逻辑使用了其中的 login 方法。trait 文件中的相关代码如下:

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

具体逻辑为首先验证表单提交字段是否通过验证,即$this->validateLogin($request)方法,下面的第一个 if 代码块用来判断是否超过登录次数限制。接着判断用户能否进行登录。即$this->attemptLogin($request)方法。

protected function attemptLogin(Request $request)
{
    return $this->guard()->attempt(
        $this->credentials($request), $request->filled('remember')
    );
}

protected function guard()
{
    return Auth::guard();
}

protected function credentials(Request $request)
{
    return $request->only($this->username(), 'password');
}

通过上面的代码可以看到,我们调用 Auth::guard() 来判断用户能否登录,如果认证通过那么用户登录成功,否则登录失败。至于这个 Guard 的工作原理,我们下面详细说明。

Auth::guard()->attempt()

AuthManager

由于 Auth 的 Facades 对应的底层类为Illuminate\Auth\AuthManager,因此我们首先分析这个类。

public function guard($name = null)
{
    $name = $name ?: $this->getDefaultDriver();

    return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
}

public function getDefaultDriver()
{
    return $this->app['config']['auth.defaults.guard'];
}

在登录的逻辑中,由于没有传入特定的参数,因为我们将会调用默认的 Driver 和 Provider。即config/auth.php中的defaults 配置:

'defaults' => [
    'guard' => 'web',
    'passwords' => 'users',
],

...

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

   ...
],

'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model' => App\User::class,
    ],
],

'passwords' => [
    'users' => [
        'provider' => 'users',
        'table' => 'password_resets',
        'expire' => 60,
    ],
],

通过配置文件可以看到,我们使用的 driver 是 session driver 和 eloquent provider。在 createSessionDriver 方法中我们新建了一个SessionGuard。
因此 Auth::guard() 返回的是一个 SessionGuard 类。

在 SessionGuard 中我们可以看到有 attempt 方法。

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

    $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);

    if ($this->hasValidCredentials($user, $credentials)) {
        $this->login($user, $remember);

        return true;
    }

    $this->fireFailedEvent($user, $credentials);

    return false;
}

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

public function login(AuthenticatableContract $user, $remember = false)
{
    $this->updateSession($user->getAuthIdentifier());

    if ($remember) {
        $this->ensureRememberTokenIsSet($user);

        $this->queueRecallerCookie($user);
    }

    $this->fireLoginEvent($user, $remember);

    $this->setUser($user);
}

在这里我们可以看到首先触发 attempt 事件。接着我们通过 EloquentUserProvider 中的retrieveByCredentials 得到除 password 字段外匹配的 User 模型。如果匹配成功,则判断密码是否一致,如果一致,则登录成功。否则失败。

注:密码校验默认使用 bcrypt。

EloquentUserProvider 中相关代码如下:

public function retrieveByCredentials(array $credentials)
{
    if (empty($credentials) ||
       (count($credentials) === 1 &&
        array_key_exists('password', $credentials))) {
        return;
    }

    $query = $this->newModelQuery();

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

        if (is_array($value) || $value instanceof Arrayable) {
            $query->whereIn($key, $value);
        } else {
            $query->where($key, $value);
        }
    }

    return $query->first();
}

public function validateCredentials(UserContract $user, array $credentials)
{
    $plain = $credentials['password'];

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

这基本上就是登录过程的原理。

原文

本作品采用《CC 协议》,转载必须注明作者和本文链接
There's nothing wrong with having a little fun.
Epona
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!