laravel默认登录注册源码解析追踪

Author Avatar
呃哦 8月 18, 2017

被laravel的登录注册烦恼了好一段时间,总算理顺了思路。

laravel内部封装实现好了一个简单版本的登录注册模块,通过以下命令即可快速开启:

php artisan make:auth

但是毕竟是默认实现好的,总想知道内部代码实现。于是开始了源码之旅。。。

Auth

先说说laravel的认证过程,门面(Facade) Auth 提供了一些便捷的方法如获得当前认证用户信息等,实际上负责认证用户的是守卫 guard ,guard负责该用户是否通过验证,是否已登录,是否 记住等。门面Auth实际上是在 AuthServiceProvider 中注册的 AuthManager 的实例

guard

关于guard的配置在config/Auth.php中,如配置默认使用哪个 guardguard 使用的驱动,框架默认实现的有 sessiontoken 两个驱动类。驱动类则负责从服务提供类中获得认证需要的数据。

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

        'api' => [
            'driver' => 'token',
            'provider' => 'users',
        ],
    ],

自定义guard类,guard需要检查用户是否登录,认证账号是否正确等,因此laravel定义了一个契约接口需要实现,自定义guard需要StatefulGuard或者Guard接口。然后在 AuthServiceProvider 中注册guard。

class AuthServiceProvider extends ServiceProvider
{
    /**
     * Register any application authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        // 'jwt'为注册guard的名字,待会在config/auth.php中配置。
        // 匿名函数返回自定义的guard类的实例
        Auth::extend('jwt', function ($app, $name, array $config) {
            // Return an instance of Illuminate\Contracts\Auth\Guard...

            return new JwtGuard(Auth::createUserProvider($config['provider']));
        });
    }
}

注册后需要在config/auth.php中配置

'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

providers

guard 负责认证用户,具体为针对请求中的认证信息,对对应数据库中的信息查询是否存在,而查询信息这种操作便是交给服务提供者来实现了。在默认配置中,设定了 ‘web’ 这个 guard 的提供者为 users 。 下面是 users 的配置。

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

users使用 eloquent驱动,默认的驱动支持eloquentdatabase。这里使用 eloquent驱动,eloquent对应使用的model类是App\User:class,即认证用户在user表中。

自定义provider,自定义类需要实现 UserProvider 接口,接口定义了如下方法需要实现:

  • retrieveById : 获取一个代表用户的值
  • retrieveByToken : 借助用户唯一的 $identifier 和「记住我」$token 来获取用户
  • updateRememberToken : 使用新的 $token 更新 $userremember_token 字段
  • retrieveByCredentials : 根据凭证数据获取一个 UserInterface 的实例。
  • validateCredentials : 比较 $user 和 $credentials 来认证这个用户。
    如上接口laravel也封装好了默认的实现类,分别是 EloquentUserProviderDatabaseUserProvider 。当providers配置驱动(driver)为 ‘eloquent’ 时即为前者,配置驱动为 ‘database’ 时使用后者。可以查看这两个类源码看具体接口实现。

自定义providers的注册与配置,自定义好providers后在AuthServiceProvider的boot方法中注册

class AuthServiceProvider extends ServiceProvider
{
    /**
     * Register any application authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Auth::provider('riak', function ($app, array $config) {
            return new RiakUserProvider($app->make('riak.connection'));
        });
    }
}

而后在config/auth.php中配置

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

自定义Model。

自定义model需要实现接口 Illuminate\Contracts\Auth\Authenticatable

系统默认实现好了一个 User model类,并使用trait Illuminate\Auth\Authenticatable 实现接口了接口中的方法,当自定义时可以在源码实现中参考。


登录认证过程

讲完上面的基础知识后,看一遍laravel生成的登录认证过程吧!

当使用命令 php artisan make:auth后,系统会生成了一些脚手架。首先是routes/web.php中,增加了一些路由如下:

Auth::routes();
Route::get('/', 'HomeController@index')->name('home');

跟中源码可得,Auth::routes()实际上是调用Illuminate/Routing/Router类的auth方法.

    /**
     * Register the typical authentication routes for an application.
     *
     * @return void
     */
    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\LoginController的showLoginForm方法。

在LoginController中会发现没有showLoginForm方法,该类使用了AuthenticatesUsers这个trait实现了Auth::routes()路由中LoginController的方法。

showLoginForm方法如下:

    /**
     * Show the application's login form.
     *
     * @return \Illuminate\Http\Response
     */
    public function showLoginForm()
    {
        return view('auth.login');
    }

返回默认生成的login视图,login视图的表单中有一行代码为

<form class="form-horizontal" role="form" method="POST" action="{{ route('login') }}">

即表单账号数据post到路由login中,回到web.php可以看到post到login对应的方法的为Auth\LoginController的login方法。方法源码如下。

    /**
     * Handle a login request to the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response
     */
    public function login(Request $request)
    {
        $this->validateLogin($request);

        // If the class is using the ThrottlesLogins trait, we can automatically throttle
        // the login attempts for this application. We'll key this by the username and
        // the IP address of the client making these requests into this application.
        if ($this->hasTooManyLoginAttempts($request)) {
            $this->fireLockoutEvent($request);

            return $this->sendLockoutResponse($request);
        }

        if ($this->attemptLogin($request)) {
            return $this->sendLoginResponse($request);
        }

        // If the login attempt was unsuccessful we will increment the number of attempts
        // to login and redirect the user back to the login form. Of course, when this
        // user surpasses their maximum number of attempts they will get locked out.
        $this->incrementLoginAttempts($request);

        return $this->sendFailedLoginResponse($request);
    }

首先是$this->validateLogin($request);,源码为:

    /**
     * Validate the user login request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return void
     */
    protected function validateLogin(Request $request)
    {
        $this->validate($request, [
            $this->username() => 'required|string',
            'password' => 'required|string',
        ]);
    }

即验证用户的请求中用户的表单填写是否正确,这里的验证规则是 $this->usernamepassword字段必须是非空且是string类型。
如果验证规则不通过,将会抛出ValidationException异常,也就停止了login()函数往下走了。$this->username字段值默认是email,可以在LoginController中重写username()方法,返回别的字段名如user_name

回到login()函数,当验证规则通过后,login函数往下走,

        if ($this->hasTooManyLoginAttempts($request)) {
            $this->fireLockoutEvent($request);

            return $this->sendLockoutResponse($request);
        }

判断该请求是否多次登录尝试,laravel有多次登录尝试时锁定一分钟不能登录的机制,这里跳过继续往下走。

        if ($this->attemptLogin($request)) {
            return $this->sendLoginResponse($request);
        }

尝试登录。登录成功则发送登录成功响应。不过不成功,login函数往下走,增加这个请求的登录尝试次数,上文提到当多次请求将被锁定。最后发送登录失败响应。

回到attemptLogin方法,

    /**
     * Attempt to log the user into the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return bool
     */
    protected function attemptLogin(Request $request)
    {
        return $this->guard()->attempt(
            $this->credentials($request), $request->has('remember')
        );
    }

调用门卫Guard进行登录尝试。这里的$this->guard()方法的实现是return Auth::guard();,再跳转到Auth::guard()方法,

    /**
     * Attempt to get the guard from the local cache.
     *
     * @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);
    }

这里获取配置中配置使用的guard,由于不指定,所以按照config/auth.php配置后默认使用的是web这个guard,web guard配置的driver是 session,所以回到上文,$this->guard()->attempt()方法实际上调用的是 SessionGuard 类中的attempt方法。

    /**
     * Attempt to authenticate a user using the given credentials.
     *
     * @param  array  $credentials
     * @param  bool   $remember
     * @return bool
     */
    public function attempt(array $credentials = [], $remember = false)
    {
        $this->fireAttemptEvent($credentials, $remember);

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

        // If an implementation of UserInterface was returned, we'll ask the provider
        // to validate the user against the given credentials, and if they are in
        // fact valid we'll log the users into the application and return true.
        if ($this->hasValidCredentials($user, $credentials)) {
            $this->login($user, $remember);

            return true;
        }

        // If the authentication attempt fails we will fire an event so that the user
        // may be notified of any suspicious attempts to access their account from
        // an unrecognized user. A developer may listen to this event as needed.
        $this->fireFailedEvent($user, $credentials);

        return false;
    }

第一行发送事件跳过,第二行$this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);通过provider的retrieveByCredentials方法根据请求中携带的凭证获得Model的实例对象。这里根据在 config/auth.php 中的配置,跳转到 EloquentUserProviderretrieveByCredentials 方法。

    /**
     * Retrieve a user by the given credentials.
     *
     * @param  array  $credentials
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function retrieveByCredentials(array $credentials)
    {
        if (empty($credentials)) {
            return;
        }

        // First we will add each credential element to the query as a where clause.
        // Then we can execute the query and, if we found a user, return it in a
        // Eloquent User "model" that will be utilized by the Guard instances.
        $query = $this->createModel()->newQuery();

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

        return $query->first();
    }

获得User这个model的查询构造器后根据凭证里面的键值对查询数据库中是否存在该对象,查询时不比对 password ,因为该字段在后面验证账号时才比对。

然后attempt方法往下走,

    public function attempt(array $credentials = [], $remember = false)
    {
        // 省略
        if ($this->hasValidCredentials($user, $credentials)) {
            $this->login($user, $remember);

            return true;
        }
        // 省略
    }

        /**
     * Determine if the user matches the credentials.
     *
     * @param  mixed  $user
     * @param  array  $credentials
     * @return bool
     */
    protected function hasValidCredentials($user, $credentials)
    {
        return ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
    }

根据凭证获得User这个模型的对象实例后,在 hasValidCredentials 方法判断实例是否为空,因为假设凭证数据在查询构造器中没有获得实例对象说明不存在这个数据。当 $user 不为空,在 validateCredentials 中,判断密码是否正确。

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

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

在源码中可以看到默认的登录注册是将提交的密码hash加密后再与数据库中的 $user 对象的 getAuthPassword() 比较。 getAuthPassword() 的实现在 Illuminate\Auth\Authenticatable 如下。

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

回到attempt函数,当认证通过后,调用login函数,login函数主要做以下几件事:

    /**
     * Log a user into the application.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  bool  $remember
     * @return void
     */
    public function login(AuthenticatableContract $user, $remember = false)
    {
        $this->updateSession($user->getAuthIdentifier());

        // If the user should be permanently "remembered" by the application we will
        // queue a permanent cookie that contains the encrypted copy of the user
        // identifier. We will then decrypt this later to retrieve the users.
        if ($remember) {
            $this->ensureRememberTokenIsSet($user);

            $this->queueRecallerCookie($user);
        }

        // If we have an event dispatcher instance set we will fire an event so that
        // any listeners will hook into the authentication events and run actions
        // based on the login and logout events fired from the guard instances.
        $this->fireLoginEvent($user, $remember);

        $this->setUser($user);
    }
  • 更新session
  • 如果登录时有勾选 Remember me 选框,
    • 生成一个token Id存储到 User 表中
    • 生成一个remember me的id到cookies中,据说是永久有效
  • 发送登录事件。

注册认证过程

注册的处理方法为 RegisterController@register 方法,同理在 trait RegistersUsers下。

    /**
     * Handle a registration request for the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    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)
                        ?: redirect($this->redirectPath());
    }

注册则较为简单,第一行先根据表单填写是否符合规则,规则在 RegisterController@validator 下,当表单填写通过规则后,调用 RegisterController@create 方法在数据库中添加一行数据。而后调用 SessionGuard@login 方法,在上面分析过了。


至此,登录注册认证过程源码追踪解析完毕!