0%

Laravel 登录原理剖析

简介

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

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

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

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

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

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

1
php artisan make:auth

原理剖析

Auth::routes()

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

1
Auth::routes();

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

1
2
3
4
5
6
7
8
9
10
11
// 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 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 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()中传入参数来控制个别路由是否注册,比如:

1
2
3
4
5
Auth::routes([
'register' => false,
'reset' => false,
'verify' => true
]);

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

登录原理

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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,因此我们首先分析这个类。

1
2
3
4
5
6
7
8
9
10
11
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 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
'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 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 中相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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());
}

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

欢迎关注我的其它发布渠道