浅析 Laravel 自带的用户认证逻辑
一、前期准备#
生成数据表#
在.env
文件中配置好数据库,执行 php artisan migrate
,此时会在数据库生成我们需要的几张表
生成文件#
安装 laravel
的 ui
包
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.css
和 app.js
文件,因为我们根本还没有生成。不急,接下来先美化一下样式。
利用 Laravel Mix 美化页面#
Laravel Mix
是什么就不解释,不知道的同学可以看文档哦!使用 Laravel Mix
之前要保证已经安装了 nodejs
和 npm
:
node -v
npm -v
如果能看到各自的版本号就代表已经安装了,如果没有,自行安装。接着运行:
npm install
安装完成后,即可运行 Mix
:
// 运行 Mix 任务
npm run dev
// 运行所有的 Mix 任务并最小化输出结果
npm run production
这里我们就运行 npm run production
吧,发现 public
下多了 css/app.css
和 js/app.js
还有 mix-manifest.json
,mix-manifest.json
是 mix
生成的文件清单,可是 css/app.css
和 js/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::$app
是 Illuminate\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
会发现,这个性状就是一个用户登录的完整的一套逻辑:显示视图、获取数据、验证数据、进行登录、登录成功响应、登录失败响应、定义门卫、定义认证字段等。该性状内部还使用了 RedirectsUsers
和 ThrottlesLogins
性状,前者的作用是重定向后者作用是限流。
在 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 表示不使用任何加密,也可以设置为 tls
或 ssl
。
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 协议》,转载必须注明作者和本文链接
推荐文章: