浅析 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 协议》,转载必须注明作者和本文链接
不二
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 3

一楼拿走 感谢楼主的分享

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

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

这确实不是个好的 demo 。

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

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

4年前 评论

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