浅析 Laravel 自带的用户认证逻辑

一、前期准备#

生成数据表#

.env 文件中配置好数据库,执行 php artisan migrate,此时会在数据库生成我们需要的几张表

生成文件#

安装 laravelui

composer require laravel/ui

生成 登录 / 注册 脚手架

php artisan ui vue --auth

此处可以使用 vue 或者 react 或者 bootstrap,在执行该命令前可以观察一下 resources/views 目录和 routes/web.php 文件内容。执行完这条命令,就会发现在根目录下多了一个 webpack.mix.js 文件,/routes/web.php 文件多了以下内容:

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

/resources/views 目录下多了 auth 文件夹、layouts 文件夹和 home.blade.php 文件。

此时,在浏览器中输入 yourdomain/home 会发现自动跳转到 login 页面了,例如我本地的开发域名设置为 laravel.do,打开 laravel.do/home 就会跳转到 laravel.do/login,不过样式... 很丑!打开调试窗口,发现报错找不到 app.cssapp.js 文件,因为我们根本还没有生成。不急,接下来先美化一下样式。

利用 Laravel Mix 美化页面#

Laravel Mix 是什么就不解释,不知道的同学可以看文档哦!使用 Laravel Mix 之前要保证已经安装了 nodejsnpm:

node -v
npm -v

如果能看到各自的版本号就代表已经安装了,如果没有,自行安装。接着运行:

npm install

安装完成后,即可运行 Mix

// 运行 Mix 任务
npm run dev

// 运行所有的 Mix 任务并最小化输出结果
npm run production

这里我们就运行 npm run production 吧,发现 public 下多了 css/app.cssjs/app.js 还有 mix-manifest.jsonmix-manifest.jsonmix 生成的文件清单,可是 css/app.cssjs/app.js 是从哪儿来的呢?上面提到根目录多了一个文件,对,就是 webpack.mix.js,里面有这么一段代码:

mix.js('resources/js/app.js', 'public/js')
   .sass('resources/sass/app.scss', 'public/css');

这段代码的意思就是将 resources/js/app.js 编译到 public/js 下并命名为 app.js 且将 resources/sass/app.scss 编译到 public/css 下并命名为 app.css,此时再打开页面,真好看!

二、分析认证#

先从路由说起#

可能有点啰嗦,其实就是为了说明如何得到那一堆路由的,不想看的可直接跳过。

前面说到 routes/web.php 文件增加了两行代码,第二行大家都知道,那么第一行的 Auth::routes(); 是什么鬼?

不急,我们知道 Auth 使用了门脸方式,那么一定有一个 Auth.php 文件在命名空间 Illuminate\Support\Facades 下。

命名空间从 vendor/laravel/framework/src 开始,继续往下找 Illuminate/Support/Facades,果然找到了 Auth.php 文件,内容不多,一个 Auth 类继承了 Facade,实现了两个方法,第一个方法 getFacadeAccessor 定义了一个门脸访问器,返回 auth 实现可以通过 auth() 辅助函数方式来访问 Auth,第二个方法 routes 就是用来生成认证路由的。

routes 方法中只有一行代码:

static::$app->make('router')->auth($options);

static::$appIlluminate\Contracts\Foundation\Application 的一个实例。

Illuminate\Contracts\Foundation\Application 又继承自 Illuminate\Contracts\Container\Container,故而 static::$app 可以调用 make 方法。make 方法利用 PHP反射机制解析出一个类,这里解析出的就是 Illuminate\Routing\Router 这个类,所以上面那一行代码最终执行的是 Illuminate\Routing\Router 类中的 auth 方法。

再来看 auth 方法。

这个方法就简单多了,里面定义了几种路由,所以 Auth::routes(); 最终得到的就是这样的一堆路由:

Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');
Route::post('login', 'Auth\LoginController@login');
Route::post('logout', 'Auth\LoginController@logout')->name('logout');
Route::get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
Route::post('register', 'Auth\RegisterController@register');
Route::get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
Route::post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
Route::get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
Route::post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update');
Route::get('password/confirm', 'Auth\ConfirmPasswordController@showConfirmForm')->name('password.confirm');
Route::post('password/confirm', 'Auth\ConfirmPasswordController@confirm');
Route::get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
Route::get('email/verify/{id}/{hash}', 'Auth\VerificationController@verify')->name('verification.verify');
Route::post('email/resend', 'Auth\VerificationController@resend')->name('verification.resend');

看着这一堆让人头大的路由,还是觉得 Auth::routes() 好啊!

再聊聊控制器#

1、HomeController

HomeController 的构造函数定义了访问中间件 auth

Kernel.php 中的 $routeMiddleware 可以看到 auth 映射的是 App\Http\Middleware\Authenticate 类,这个类继承了 Illuminate\Auth\Middleware\Authenticate 类,并定义了 redirectTo 方法。

打开 Illuminate\Auth\Middleware\Authenticate 类,我们可以看到这个类很简单,先是注入了 Illuminate\Contracts\Auth\Factory 的对象,然后在 handle 方法中调用了自身的 authenticate 方法,如果认证成功则返回用户,认证失败再调用 unauthenticated,该方法会抛出 AuthenticationException 异常,将 redirectTo 方法返回的结果作为第三个参数传入其中。AuthenticationException 会调用 redirectTo 方法进行跳转。

关于为什么会调用 redirectTo 进行跳转,可以阅读一下这个类 Illuminate\Foundation\Exceptions\Handler

此时再看 Illuminate\Auth\Middleware\Authenticate 就清楚多了。

protected function redirectTo($request)
{
    if (! $request->expectsJson()) {
        return route('login');
    }
}

redirectTo 方法覆盖了父类方法,定义了未认证用户如何跳转。此处的意思是如果我们没有期望返回值是 json 数据的话就返回命名为 login 的路由即:

Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');

2、LoginController

LoginController 的构造函数定义了除了 logout 路由其它都访问 guest 这个中间件。

Kernel.php 中可以看到 guest 映射的是 App\Http\Middleware\RedirectIfAuthenticated 类,该类就做一件事,判断用户如果已经登录则跳转到 /home

LoginController 还定义了一个保护属性 $redirectTo 和使用了一个性状 AuthenticatesUsers

打开 Illuminate\Foundation\Auth\AuthenticatesUsers 会发现,这个性状就是一个用户登录的完整的一套逻辑:显示视图、获取数据、验证数据、进行登录、登录成功响应、登录失败响应、定义门卫、定义认证字段等。该性状内部还使用了 RedirectsUsersThrottlesLogins 性状,前者的作用是重定向后者作用是限流。

LoginController 中我们可以重定义 showLoginForm 方法来显示其他视图,可以重定义 validateLogin 方法来规定数据验证规则,可以重定义 username 方法来指定要认证的字段,可以重定义 guard 方法来重定义门卫等。

不过,目前数据库中是空的,先点击页面右上方的 Register 去注册个用户吧!点击之后,我们还是先想想 Laravel 做了什么吧!

3、RegisterController

RegisterController 的构造函数定义了访问中间件 guest,定义了跳转地址 $redirectTo = '/home',定义了一个数据验证器 validator 和生成用户记录,同时也使用一个性状 RegistersUsers

照例打开 Illuminate\Foundation\Auth\RegistersUsers,再看这个性状就容易多了,也是定义注册用户的一套逻辑:显示视图、定义门卫、进行注册、已注册处理。进行注册的方法 register 内部分为 4 步:数据验证、注册成功事件监听、用户登录,注册成功后页面跳转。

注册成功事件监听使用的是 Illuminate\Auth\Events\Registered 类,在 App\Providers\EventServiceProvider 服务提供者的监听器中定义了事件映射:

protected $listen = [
    Registered::class => [
        SendEmailVerificationNotification::class,
    ],
];

Registered 事件完成时,触发 SendEmailVerificationNotification,顾名思义就是发送邮箱验证通知。

接着我们在注册页面输入好信息,点击 Register 页面直接跳转到了 /home,现在去我们刚刚注册的邮箱里看下,按道理来说是应该收到一封邮件验证邮箱的邮件的,但现在什么也没有,这是怎么回事呢?

打开 Illuminate\Auth\Listeners\SendEmailVerificationNotification 看看就知道了。

public function handle(Registered $event)
{
    if ($event->user instanceof MustVerifyEmail && ! $event->user->hasVerifiedEmail()) {
        $event->user->sendEmailVerificationNotification();
    }
}

我们创建的用户必须是接口 Illuminate\Contracts\Auth\MustVerifyEmail 的实例且邮箱还没有被验证才会发送邮件。

打开模型 App\User 我们会发现无论是 App\User 本身还是他继承的 Illuminate\Foundation\Auth\User 都没有实现接口 Illuminate\Contracts\Auth\MustVerifyEmail,所以才没有发送验证邮件,怎么改造一下就可以送了,自己动动脑筋吧!

到这里,我们就已经搞清楚用户注册登录的整个过程了,去登录页输入自己刚刚注册的用户,即可成功登录。接下来就该说说忘记密码这一块的逻辑了。

我们点击 Forgot Your Password? 发现跳转到了 laravel.do/password/reset,输入之前注册用户时使用的邮箱,点击 Send Password Reset Link,等了一会儿页面直接报了一个错:

Expected response code 250 but got code "530", with message "530 5.7.1 Authentication required
 "

这是怎么回事呢?

4、ForgotPasswordController

password/reset 访问的是 ForgotPasswordController 控制器,可是 ForgotPasswordController 简单的让人头皮发麻,就使用了一个性状 SendsPasswordResetEmails。不慌,打开这个性状你就会镇定多了,依旧是 Laravel 实现的一套完整的忘记密码的逻辑:显示视图、获取邮箱、验证邮箱、发送邮件、发送成功响应、发送失败响应等。

那么为什么会报错呢?

因为还没有配置邮箱!

.env 中配置一下邮箱:

MAIL_DRIVER=smtp
MAIL_HOST=smtp.163.com
MAIL_PORT=25
MAIL_USERNAME=******
MAIL_PASSWORD=******
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=******@163.com
MAIL_FROM_NAME=Laravel社区

MAIL_DRIVER 用于配置默认的邮件发送驱动,此处的最佳选择是 SMTP,其它的要么收费要么不能使用。

MAIL_HOST 是邮箱所在主机,对应值是 smtp.163.com

MAIL_PORT 用于配置邮箱发送服务端口号,比如一般默认值是 25,但如果 MAIL_ENCRYPTION 的值是 ssl,该值为 465。

MAIL_USERNAME 表示邮箱账号,比如 ******@163.com

MAIL_PASSWORD 表示上述邮箱登录对应登录密码。注意使用的是客户端授权密码

MAIL_ENCRYPTION 表示加密类型,可以设置为 null 表示不使用任何加密,也可以设置为 tlsssl

MAIL_FROM_ADDRESS 表示发送邮件使用的邮箱。

MAIL_FROM_NAME 表示发送邮件使用的名称。

现在再去忘记密码页面输入邮箱点击发送就不会报错了。进入自己的邮箱也会收到一封 Laravel 内置的漂亮的邮件,打开邮件,点击 Reset Password,有可能出现一个不是我们预期的页面,原因也很简单,因为发送邮件的时候 Laravel 提取了域名拼接成了一个完整的 URL,而域名的配置在 app.php 中:

'url' => env('APP_URL', 'http://localhost')

此处使用了 env 辅助函数从.env 文件中读取 APP_URL 配置,如果没有则使用默认值 http://localhost。那么我们去.env 中把 APP_URL 换成 http://laravel.do(这个是我本地设置的域名)即可。再重新发送一封邮件,点击 Reset Password 就能进入我们预期的页面了。

5、ResetPasswordController

ResetPasswordController 中还是使用了性状 ResetsPasswords 来负责完成重置密码的逻辑。Illuminate\Foundation\Auth\ResetsPasswords 的实现逻辑也很简单,这里就不做详细的介绍了,大家可以自行理解。

三、一点想法#

Laravel 用户认证的浅析到这里就结束了,我们可以在控制器中覆盖性状中的方法来实现定制化开发,比如使用新的试图,使用新的认证规则等,而这一切的实现也仅仅重新定义一个方法这么简单,这就是使用 Laravel 的魔力。

在没有接触 Laravel 之前根本没用过 trait 官方手册也看明白这是个啥,但就是不知道真实的使用场景,是 Laravel 让我知道它的使用场景。而 Laravel 带给我的也远不止这些,自从半年前接触到 Laravel 就深深地喜欢上了这款框架,喜欢它并不仅仅因为它很优美,更多的是它的设计思想和高效的开发,高效的开发带来的好处自不必说,设计思想又可以让我们学习到很多优秀的思想或者说实现方式,于我们能力的提升是有莫大的好处的,至于运行效率,我先是赞同这句话 —— 程序是写给人看的,机器只是恰巧能够运行。随着硬件和 PHP 自身的发展还有这么多的优化手段,我想性能不应该是我们开发者首要考虑的因素

这一次也通过认真阅读源码,发现阅读源码不但没那么可怕,反而会让我从中不断找到乐趣,从猜想到猜想被验证,每次都是一种肯定和成长。个人感觉从一个小功能入手阅读源码可能会比较容易一点。

这是我第一次分享技术的文章,写了整整半天,现在才体会到分享技术的不易,给乐于分享的技术者们点赞并向他们学习,文章之中如果有不对或不足之处还请大佬们指正,以免误导他人!

本作品采用《CC 协议》,转载必须注明作者和本文链接
不二
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 3

一楼拿走 感谢楼主的分享

5年前 评论
ztlcoder (楼主) 5年前

我最近也在看这个。这么复杂的东西,现实工程中有人用么?自己起一套比他这一套还容易懂。比如改成符合中国国情的手机验证码注册。或者更简单的用户名密码登录。

这确实不是个好的 demo 。

5年前 评论
ztlcoder (楼主) 5年前

-- 看了官方文档是 implements MustVerifyEmail 但是加了 wordpress。控制器这里要怎么解决啊。能不能说一下啊。。

5年前 评论