二、注册登录

二、注册登录

一、用户认证脚手架

1、用户认证脚手架

Laravel 自带了用户认证功能,我们将利用此功能来快速构建我们的用户中心。

首先执行认证脚手架命令,生成代码:

$ php artisan make:auth

命令 make:auth 会询问我们是否要覆盖 app.blade.php,因为我们在前面章节中已经自定义了『主要布局文件』—— app.blade.php,所以此处输入 no,如下:

image.png

使用 git status 来查看文件更改的状态:

image.png

打开 routes/web.php 查看修改了哪些内容:

routes/web.php

<?php

Route::get('/', 'PagesController@root')->name('root');
Auth::routes();

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

可以看到在我们的主页下,多了两个表达式,先看第一个:

Auth::routes();

此处是 Laravel 的用户认证路由,可以在 vendor/laravel/framework/src/Illuminate/Routing/Router.php 中搜索关键词 LoginController 即可找到定义的地方,以上等同于:

// 用户身份验证相关的路由
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');

// Email 认证相关路由
Route::get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
Route::get('email/verify/{id}', 'Auth\VerificationController@verify')->name('verification.verify');
Route::get('email/resend', 'Auth\VerificationController@resend')->name('verification.resend');

为了更加直观,我们将在 web.php 中使用以上替换 Auth::routes();

再来看下面这一行:

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

我们已经有自己的主页了,不需要再次设置主页,直接删除即可。修改后的路由文件内容如下,请直接替换:

routes/web.php

<?php

Route::get('/', 'PagesController@root')->name('root');

// 用户身份验证相关的路由
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');

// Email 认证相关路由
Route::get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
Route::get('email/verify/{id}', 'Auth\VerificationController@verify')->name('verification.verify');
Route::get('email/resend', 'Auth\VerificationController@resend')->name('verification.resend');

2、生成的视图

make:auth 命令为我们生成了 resources/views/auth 下的四个文件:

视图名称说明
register.blade.php注册页面视图
login.blade.php登录页面视图
verify.blade.php邮箱认证视图
passwords/email.blade.php提交邮箱发送邮件的视图
passwords/reset.blade.php重置密码的页面视图


3、移除无用页面

因为无需使用 make:auth 生成的主页,请运行以下命令删除无用文件:

$ rm app/Http/Controllers/HomeController.php
$ rm resources/views/home.blade.php

4、顶部导航

顶部导航『注册』和『登录』入口之前用的是两个假链接,现在我们已经有业务逻辑,接下来将链接套进去,并加上登录后的效果:

resources/views/layouts/_header.blade.php

<nav class="navbar navbar-expand-lg navbar-light bg-light navbar-static-top">
    <div class="container">
        <!-- Branding Image -->
        <a class="navbar-brand " href="{{ url('/') }}">
            LaraBBS
        </a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>

        <div class="collapse navbar-collapse" id="navbarSupportedContent">
            <!-- Left Side Of Navbar -->
            <ul class="navbar-nav mr-auto">

            </ul>

            <!-- Right Side Of Navbar -->
            <ul class="navbar-nav navbar-right">
                <!-- Authentication Links -->
                <li class="nav-item"><a class="nav-link" href="{{route('login')}}">登录</a></li>
                <li class="nav-item"><a class="nav-link" href="{{route('register')}}">注册</a></li>
            </ul>
        </div>
    </div>
</nav>

现在通过顶部导航访问登录页面,看看效果:

image.png

5、本地化

可以看到登录表单是英文版本的,打开模板文件,此模板文件是我们刚刚使用 make:auth命令生成的:

resources/views/auth/login.blade.php

可以看到很多 __() 函数的调用:

<div class="card-header">{{ __('Login') }}</div>
<label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>
{{ __('Remember Me') }}

这是 Laravel 提供的本地化特性,使用 __() 函数来辅助实现。按照约定,本地化文件存储在 resources/lang 文件夹中,为 JSON 格式。在 config/app.php 文件中,我们设置了:

'locale' => 'zh-CN',

对应翻译文件就是 resources/lang/zh-CN.json ,需新建此文件:

resources/lang/zh-CN.json

{
  "Login": "登录",
  "Password": "密码",
  "Remember Me": "记住我"
}

再次刷新登录页面,可以看到翻译的内容,同时红框里也是我们漏下的内容:

image.png

6、中文语言包

会有很多人会遇到翻译 Laravel 自带模板的问题,所以我们无需自己一个个去翻译,这种通用的问题找找扩展包来处理即可。

我们将使用 Laravel Lang 项目来实现,此项目支持了 52 个国家的语言,使用以下命令安装:

$ composer require "overtrue/laravel-lang:~3.0"

安装完成后刷新页面,完美翻译:

image.png



点击上图的忘记密码链接,进入忘记密码的视图,也可以看到成功显示中文:

image.png

Laravel Lang 同自定义语言包一样,都是根据 config/app.php 里 locale 的选项来选择语言的。

值得一提的是,如果你想修改扩展包提供的语言文件,可以使用以下命令发布语言文件到项目里:

$ php artisan lang:publish zh-CN

发布后的语言文件存放于 resources/lang/zh-CN 文件夹。

Git 版本控制

至此注册登录功能已经完成,接下来把代码纳入到版本管理:

$ git add -A
$ git commit -m "生成用户认证代码"

二、用户注册

1、测试注册功能

点击页面右上角的 注册按钮 进入注册页面,并填写表单:

image.png


点击『注册』按钮提交表单,将会出现下图报错:

image.png

这是 Laravel 框架内部集成的异常处理扩展 whoops 所渲染出来的视图。日常开发中,我们会有大量的机会跟此工具打交道,接下来我们一起来熟悉一下。

2、whoops

whoops 是一个非常优秀的 PHP Debug 扩展,它能够使你在开发中快速定位出错的位置。

image.png

  • 区域 1 —— 是错误异常的简介

  • 区域 2 —— 是错误发生的位置

  • 区域 3 —— 是程序调用堆栈,这里看到脚本调用的顺序

  • 区域 4 —— 是一些运行环境的信息,包括:

    • GET Data —— 用户提交的 GET 请求,PHP 超级全局变量 $_GET 里的内容

    • POST Data —— 表单提交的数据,PHP 超级全局变量 $_POST 里的内容

    • Files —— 用户上传文件的数据,PHP 超级全局变量 $_FILES 里的内容

    • Cookies —— 当前用户的 Cookie 信息,PHP 超级全局变量 $_COOKIE 里的内容

    • Session —— 当前用户会话信息,PHP 超级全局变量 $_SESSION 里的内容

    • Server/Request Data —— PHP 超级全局变量 $_SERVER 里的内容

    • Environment Variables —— 项目 .env 里的内容

3、修复错误


介绍完 whoops ,我们重新回到我们的注册流程中。重点看下报错信息,也就是『区域 1』中的内容:

Illuminate \ Database \ QueryException (42S02)
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'larabbs.users' doesn't exist (SQL: select count(*) as aggregate from users where email = summer@learnku.com)

QueryException 是数据库查询语句执行出错异常。上面的简介中,重点在 Table 'larabbs.users' doesn't exist ,数据库 larabbs 中表 users 不存在。

注意数据库 larabbs 是在 Homestead 初始化时为我们创建的,我们在 Homestead.yaml 中配置过。

命令行进入 MySQL 终端查看:

$ mysql -u homestead -p

mysql 是 MySQL 终端命令的调用名称, 参数 -u 是指定用户,homestead 是 Homestead 虚拟机中为我们准备好的 MySQL 用户,-p 参数是告知我们将要为 homestead 用户输入密码。

按回车键以后,命令行将会要求你输入 MySQL 密码,密码在 .env 文件里的 DB_PASSWORD 选项中可以找到,是 secret,输入密码后按回车,就能进入 MySQL 命令行终端。

在 MySQL 命令行终端里,命令行提示符号为 mysql>,在接下来的章节中,如果你遇到此命令行提示符,请识别为此命令必须在 MySQL 命令行终端里运行。

接下来查看我们的 larabbs 数据库中,是否有数据库表存在:

mysql> use larabbs;
mysql> show tables;

执行的结果如下图:

image.png

Empty set 意味着没有任何数据。这是必然的,因为我们还未执行 artisan migrate 命令,接下来我们开始执行数据迁移来创建数据库表结构。

先退出 MySQL 命令行终端:

mysql> exit;

执行迁移:

$ php artisan migrate

image.png

执行成功,此时再重新进入 MySQL 命令行终端查看数据库表的情况:

image.png

数据库表结构已经创建成功,进入浏览器,刷新错误页面以此来重新提交我们的表单(如果你意外关闭了错误页面,只需要重新填写表单再次提交即可):

image.png


显示 404 页面未找到,我们能看到地址栏链接为 http://larabbs.test/home ,因我们自定义了主页,make:auth 生成的 home 主页文件已经被我们删除,而默认的业务逻辑是在注册成功后,直接跳转到 home 主页,接下来我们需要修改这些逻辑。

在编辑器中使用全局搜索,可以看到如下:

image.png

请将上面显示的五处地方的 '/home' 字串修改为 /,请勿使用全局替换。

修改后效果如以下:

image.png

4、登录状态

Laravel 默认注册后的用户是已登录的,接下来我们要制作顶部导航栏来响应用户的登录状态:

resources/views/layouts/_header.blade.php

<nav class="navbar navbar-expand-lg navbar-light bg-light navbar-static-top">
    <div class="container">
        <!-- Branding Image -->
        <a class="navbar-brand " href="{{ url('/') }}">
            LaraBBS
        </a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>

        <div class="collapse navbar-collapse" id="navbarSupportedContent">
            <!-- Left Side Of Navbar -->
            <ul class="navbar-nav mr-auto">

            </ul>

            <!-- Right Side Of Navbar -->
            <ul class="navbar-nav navbar-right">
                <!-- Authentication Links -->

                @guest
                    <li class="nav-item"><a class="nav-link" href="{{route('login')}}">登录</a></li>
                    <li class="nav-item"><a class="nav-link" href="{{route('register')}}">注册</a></li>
                @else
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                            <img src="https://iocaffcdn.phphub.org/uploads/images/201709/20/1/PtDKbASVcz.png?imageView2/1/w/60/h/60" class="img-responsive img-circle" width="30px" height="30px">
                            {{Auth::user()->name}}
                        </a>
                        <div class="dropdown-menu" aria-labelledby="navbarDropdown">
                            <a class="dropdown-item" href="">个人中心</a>
                            <a class="dropdown-item" href="">编辑资料</a>
                            <div class="dropdown-divider"></div>
                            <a class="dropdown-item" id="logout" href="#">
                                <form action="{{route('logout')}}" method="POST">
                                    {{csrf_field()}}
                                    <button class="btn btn-block btn-danger" type="submit" name="button">退出</button>
                                </form>
                            </a>
                        </div>
                    </li>
                @endguest
            </ul>
        </div>
    </div>
</nav>

如果是未登录用户的话,就显示注册和登录按钮,如果是已登录用户的话,即显示用户菜单。

5、登录注册用户

重新打开首页 http://larabbs.test/ ,点击右上角的下拉菜单中的『退出登录』按钮:

image.png

点击右上角 登录 按钮,填写上一步注册时使用的邮箱和密码:

image.png

点击 Login 按钮提交进行登录:

image.png

能看到我们用户名和乔布斯老爷子的头像,意味着登录成功。

Git 版本控制

至此注册登录功能已经完成,接下来把代码纳入到版本管理:

$ git add -A
$ git commit -m "修复跳转链接"

三、注册验证码

1、问题说明

我们的注册功能存在一个问题,因我们表单未添加任何防护,恶意用户可以轻易使用机器人自动化注册新用户。机器人自由注册,对我们站点稳定性来讲是巨大的威胁,恶意用户可以很轻易的通过机器人程序在短时间内,注册大量用户,甚至于填满我们的数据库。

2、验证码

image.png

验证码 是防止恶意破解密码、刷票、论坛灌水、刷页的手段。验证码有 多种类型。 本项目中我们将使用图片验证码,其原理是让用户输入一个扭曲变形的图片上所显示的文字或数字,扭曲变形是为了避免被光学字符识别软件(OCR)自动辨识。由于计算机无法识别验证码的图片,所以回答出问题的用户就可以被认为是人类。

接下来我们将使用验证码来防卫的用户注册功能。

3、安装扩展包

我们将以第三方扩展包 mews/captcha 作为基础来实现 Laravel 中的验证码功能。

使用 Composer 安装:

$ composer require "mews/captcha:~2.0"

运行以下命令生成配置文件 config/captcha.php

$  php artisan vendor:publish --provider='Mews\Captcha\CaptchaServiceProvider'

我们可以打开配置文件,查看其内容:

config/captcha.php

$  php artisan vendor:publish --provider='Mews\Captcha\CaptchaServiceProvider'

我们可以打开配置文件,查看其内容:

config/captcha.php

<?php

return [

    'characters' => '2346789abcdefghjmnpqrtuxyzABCDEFGHJMNPQRTUXYZ',

    'default'   => [
        'length'    => 9,
        'width'     => 120,
        'height'    => 36,
        'quality'   => 90,
        'math'      => true,
    ],

    'flat'   => [
        'length'    => 6,
        'width'     => 160,
        'height'    => 46,
        'quality'   => 90,
        'lines'     => 6,
        'bgImage'   => false,
        'bgColor'   => '#ecf2f4',
        'fontColors'=> ['#2c3e50', '#c0392b', '#16a085', '#c0392b', '#8e44ad', '#303f9f', '#f57c00', '#795548'],
        'contrast'  => -5,
    ],

    'mini'   => [
        'length'    => 3,
        'width'     => 60,
        'height'    => 32,
    ],

    'inverse'   => [
        'length'    => 5,
        'width'     => 120,
        'height'    => 36,
        'quality'   => 90,
        'sensitive' => true,
        'angle'     => 12,
        'sharpen'   => 10,
        'blur'      => 2,
        'invert'    => true,
        'contrast'  => -5,
    ]

];

可以看到这些配置选项都非常通俗易懂,characters 选项是用来显示给用户的所有字符串,defaultflatminiinverse 分别是定义的四种验证码类型,你可以在此修改对应选项自定义验证码的长度、背景颜色、文字颜色等属性,在此不做过多叙述。

4、页面嵌入

此扩展包的使用分为两步:

  1. 前端展示 —— 生成验证码给用户展示,并收集用户输入的答案;

  2. 后端验证 —— 接收答案,检测用户输入的验证码是否正确。

(1)前端展示

接下来我们请将注册页面模板 register.blade.php 内容替换为以下:

resources/views/auth/register.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Register') }}</div>

                <div class="card-body">
                    <form method="POST" action="{{ route('register') }}">
                        @csrf

                        <div class="form-group row">
                            <label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label>

                            <div class="col-md-6">
                                <input id="name" type="text" class="form-control{{ $errors->has('name') ? ' is-invalid' : '' }}" name="name" value="{{ old('name') }}" required autofocus>

                                @if ($errors->has('name'))
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $errors->first('name') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>

                            <div class="col-md-6">
                                <input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required>

                                @if ($errors->has('email'))
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $errors->first('email') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>

                            <div class="col-md-6">
                                <input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required>

                                @if ($errors->has('password'))
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $errors->first('password') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>

                            <div class="col-md-6">
                                <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required>
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="captcha" class="col-md-4 col-form-label text-md-right">验证码</label>

                            <div class="col-md-6">
                                <input id="captcha" class="form-control{{ $errors->has('captcha') ? ' is-invalid' : '' }}" name="captcha" required>

                                <img class="thumbnail captcha mt-3 mb-2" src="{{ captcha_src('flat') }}" onclick="this.src='/captcha/flat?'+Math.random()" title="点击图片重新获取验证码">

                                @if ($errors->has('captcha'))
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $errors->first('captcha') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-6 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Register') }}
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

代码讲解:

  1. 我们首先将此文件里的英文翻译为中文;

  2. 在『确认密码』区块代码下,我们增加了『验证码』区块代码;

  3. captcha_src() 方法是 mews/captcha 提供的辅助方法,用于生成验证码图片链接;

  4. 『验证码』区块中 onclick() 是 JavaScript 代码,实现了点击图片重新获取验证码的功能,允许用户在验证码太难识别的情况下换一张图片试试。

image.png

我们需要调整下样式:

resources/sass/app.scss

/* User register page */
.register-page {
  img.captcha {
    cursor: pointer;
    border: 1px solid #ced4da;
    border-radius: 4px;
    padding: 3px;
  }
}

因涉及到样式代码编译,请确保虚拟机里的 $ npm run watch-poll 命令处于运行中。

刷新注册页面 http://larabbs.test/register ,即可看到样式已经正常:

image.png

(2)后端验证

前端展示部分我们已经开发完毕,接下来处理后端验证逻辑。 mews/captcha 是专门为 Laravel 量身定制的扩展包,能很好的兼容 Laravel 生成的注册逻辑。我们只需要在注册的时候,添加上表单验证规则即可:

app/Http/Controllers/Auth/RegisterController.php

protected function validator(array $data)
{
    return Validator::make($data, [
        'name' => ['required', 'string', 'max:255'],
        'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
        'password' => ['required', 'string', 'min:6', 'confirmed'],
        'captcha' => ['required','captcha']
    ],[
        'captcha.required'=>'验证码不能为空',
        'captcha.captcha'=>'请输入正确的验证码'
    ]);
}

我们添加了验证规则:

'captcha' => ['required', 'captcha'],


表达式里的第二个 captcha 是 mews/captcha 自定义的表单验证规则。扩展包非常巧妙地利用了 Laravel 表单验证器提供的 自定义表单验证规则 功能。令我们在开发验证码时非常方便。

Validator 表单验证的 make() 方法第三个参数是自定义错误提示,这里我们对验证码的错误提示进行自定义。

接下来测试:

  1. 访问注册页面 http://larabbs.test/register ;

  2. 填入测试数据,验证码填写框中随便填写错误的数据;

  3. 点击注册按钮提交注册表单:

image.png

继续测试:

  1. 访问注册页面 http://larabbs.test/register ;

  2. 填入测试数据,填写正确的验证码;

  3. 点击注册按钮提交注册表单:

image.png


注册成功。至此注册验证码开发完毕。

Git 版本控制

下面把代码纳入到版本管理:

$ git add -A
$ git commit -m "注册验证码"

四、数据库视图管理工具

Homestead 虚拟机里的 MySQL 数据库服务器连接方式为:

  • Host: 127.0.0.1

  • Port: 33060

  • User: homestead

  • Pass: secret

注意此处使用了 VirtualBox 虚拟机的『端口转发』功能,Homestead 脚本默认将本机端口 33060 转发到虚拟机里的 3306 端口。所以,只要我们连接本机的 33060 端口,即可读取虚拟机中的 MySQL 数据库。(3306 是默认的 MySQL 端口)

五、邮箱认证

1、邮箱认证


从产品设计上讲,『邮箱认证』能让我们有效地检验用户邮箱的真实性,后续网站可以利用这些真实邮箱来联系上用户,例如评论触发邮件通知,或者重要信件等。

另一方面,『邮箱认证』也会对不良用户起到很好的抑制,此类用户注册后会在网站上创建大量垃圾内容,认证其邮箱,提高了注册用户的难度,有效提高网站内容的品质。

我们将只允许邮箱认证通过的用户使用网站,未认证用户会被引导进入验证邮箱页面。

『邮箱认证』工作机制一般分两步:

  1. 发送认证邮件 —— 将附带认证信息的『认证链接』发送到用户邮箱里;

  2. 检测认证链接 —— 用户打开邮件,点击认证链接进入网站,程序检测 URL 中认证参数的合法性,并渲染对应的页面。

以上流程非常通用,Laravel 默认自带了这个功能,我们可以很方便地进行集成。

屡一下产品思路:

  1. 用户注册成功后,给用户发送一个认证邮件;

  2. 用户登录状态下,如邮箱未认证,重定向到提醒验证邮箱的页面中。

接下来让我们一起来动手开发吧。

2、修改模型存放位置

Laravel 为我们生成了用户模型文件 app/User.php ,默认情况下,Laravel 会将生成的模型文件放置在 app 文件夹下。为了遵循 MVC 的开发范式,本教程中将统一使用 app/Models文件夹来放置所有的模型文件。现在让我们先来创建一个 app/Models 文件夹,并将 User.php 文件放置到其中。

$ mkdir app/Models
$ mv app/User.php app/Models/User.php

在执行完这一步的操作之后,我们还需要执行下面这两个操作:

(1)、修改 User.php 文件,更改 namespace 为我们新创建的文件夹路径:

app/Models/User.php

<?php
namespace App\Models;

(2)、编辑器全局搜索 App\User 替换为 App\Models\User,在 Sublime Text 中可使用快捷键 shift + cmd(ctrl) + f 来进行全局搜索替换的操作。

完成之后,点击右下角的 Replace 按钮替换全部:

image.png

因为上面的文件改动较大,因此我们需要进行一次 Git 提交,该改动的代码进行保存。

$ git add -A
$ git commit -m "移动 User 模型到 app/models 目录"

3、修改User模型

接下来我们将修改 User 模型,将 Laravel 自带的邮箱认证功能集成到我们的程序中。

app/Models/User.php

<?php

namespace App\Models;

use Illuminate\Notifications\Notifiable;
use Illuminate\Auth\MustVerifyEmail as MustVerifyEmailTrait;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Contracts\Auth\MustVerifyEmail as MustVerifyEmailContract;

class User extends Authenticatable implements MustVerifyEmailContract
{
    use Notifiable, MustVerifyEmailTrait;

    protected $fillable = [
        'name', 'email', 'password',
    ];

    protected $hidden = [
        'password', 'remember_token',
    ];
}

代码详解:

use Notifiable, MustVerifyEmailTrait;

加载使用 MustVerifyEmail trait,打开 vendor/laravel/framework/src/Illuminate/Auth/MustVerifyEmail.php 文件,可以看到以下三个方法:

  • hasVerifiedEmail() 检测用户 Email 是否已认证;

  • markEmailAsVerified() 将用户标示为已认证;

  • sendEmailVerificationNotification() 发送 Email 认证的消息通知,触发邮件的发送。

得益于 PHP 的 trait 功能,User 模型在 use 以后,即可使用以上三个方法。

class User extends Authenticatable implements MustVerifyEmailContract

可以打开 vendor/laravel/framework/src/Illuminate/Contracts/Auth/MustVerifyEmail.php ,可以看到此文件为 PHP 的接口类,继承此类将确保 User 遵守契约,拥有上面提到的三个方法。

4、发送认证邮件
接下来我们来开发用户注册成功后,发送认证邮件的功能。目前我们使用了 Laravel 自带的 RegisterController ,控制器通过加载 Illuminate\Foundation\Auth\RegistersUserstrait 来引入框架的注册功能,此时我们打开此 trait 来翻阅源码并定位到 register(Request $request) 方法:

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());
}

此方法处理了用户提交表单后的逻辑,我们把重点放在 event(new Registered($user = $this->create($request->all())));,这里使用了 Laravel 的事件系统,触发了 Registered 事件。

打开 app/Providers/EventServiceProvider.php 文件,此文件的 $listen 属性里我们可以看到注册了 Registered 事件的监听器:

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

打开 SendEmailVerificationNotification 类,阅读其源码:

vendor/laravel/framework/src/Illuminate/Auth/Listeners/SendEmailVerificationNotification.php

<?php

namespace Illuminate\Auth\Listeners;

use Illuminate\Auth\Events\Registered;
use Illuminate\Contracts\Auth\MustVerifyEmail;

class SendEmailVerificationNotification
{
    /**
     * Handle the event.
     *
     * @param  \Illuminate\Auth\Events\Registered  $event
     * @return void
     */
    public function handle(Registered $event)
    {
        if ($event->user instanceof MustVerifyEmail && ! $event->user->hasVerifiedEmail()) {
            $event->user->sendEmailVerificationNotification();
        }
    }
}

可以看出 Laravel 默认已经为我们设置了邮件发送的逻辑,接下来我们来测试一下。

5、开始测试

测试之前,我们先设置下邮件发送到 log 中,以便后面的测试:

.env

MAIL_DRIVER=log

修改成功后浏览器访问 http://larabbs.test/register ,填写表单并注册一个测试用户:
image.png

注册成功后,打开日志存放目录 storage/logs ,打开最近一天的 .log 文件,在文件最尾部应可见类似以下:

image.png

邮件成功发送,复制以上的激活链接,找个地方记录起来,先不要浏览器访问,后续课程会用到。

6、强制用户认证


我们希望用户认证邮箱后,才能使用网站。首先我们来验证下当前登录的用户是否是已经认证过的,做个小测试:

app/Http/Controllers/PagesController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PagesController extends Controller
{
    public function root()
    {
        dd(\Auth::user()->hasVerifiedEmail());
        return view('pages.root');
    }
}

刷新页面可以看到:
image.png

false 代表着还未激活,跟我们预想的一样,接下来我们检出还原 PagesController

$ git checkout app/Http/Controllers/PagesController.php


接下来我们将使用 Laravel 中间件 来过滤用户的所有请求,如果用户未认证的话,就跳转到邮件认证提醒的页面中。中间件是 Laravel 中开发经常使用的功能,此处我们先混个脸熟,后面章节我们会有详细讲解的篇幅。

可以使用以下命令来新建一个中间件:

$ php artisan make:middleware EnsureEmailIsVerified

打开生成的文件并代入以下内容:

app/Http/Middleware/EnsureEmailIsVerified.php

<?php

namespace App\Http\Middleware;

use Closure;

class EnsureEmailIsVerified
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        /**
         * 三个判断
         * 1、如果用户已经登录
         * 2、并且还未认证Email
         * 3、并且访问的不是email验证相关URL或者退出的URL
         */
        if ($request->user() && !$request->user()->hasVerifiedEmail() && !$request->is('email/*','logout')){
            return $request->expectsJson()?abort(403,'您的电子邮件地址未验证'):redirect()->route('verification.notice');
        }
        return $next($request);
    }
}

接下来注册中间件,注册的时机确保在 StartSession 后面即可:

app/Http/Kernel.php

protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        // \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \App\Http\Middleware\EnsureEmailIsVerified::class,
    ],

    'api' => [
        'throttle:60,1',
        'bindings',
    ],
];

刷新页面,即可看到认证提醒,并且除了我们上面代码中设置的 URL 外都会进入此页面:
image.png

内置邮箱认证还有个小功能,当你点击点击多次『重新发送 Email』后,系统会自动做限额处理,你可以在 VerificationController 中配置相应的信息:

image.png

7、最后测试

找到我们在日志里获取到的 URL ,黏贴浏览器里访问,我们将可以再次访问到首页:

image.png


Git 代码版本控制

接着让我们将本次更改纳入版本控制中:

$ git add -A
$ git commit -m "邮箱认证"

六、认证后的提示

1、消息提示

在上一节中我们开发了邮件认证功能,不过还有一点瑕疵 —— 认证成功后,没有消息提醒,感觉很突兀。

本节课我们来对此问题进行优化。

2、阅读源码

打开 VerificationController ,此控制器处理所有邮件认证相关逻辑:

app/Http/Controllers/Auth/VerificationController.php

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\VerifiesEmails;

class VerificationController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Email Verification Controller
    |--------------------------------------------------------------------------
    |
    | This controller is responsible for handling email verification for any
    | user that recently registered with the application. Emails may also
    | be re-sent if the user didn't receive the original email message.
    |
    */

    use VerifiesEmails;

    /**
     * Where to redirect users after verification.
     *
     * @var string
     */
    protected $redirectTo = '/';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('signed')->only('verify');
        $this->middleware('throttle:6,1')->only('verify', 'resend');
    }
}


源码解析:

构建函数里使用了三个中间件,并且使用了中间件简称,这些简称是在 app/Http/Kernel.php 中的 $routeMiddleware 属性里做了定义,以下是三个中间件调用的解释:

 $this->middleware('auth');

设定了所有的控制器动作都需要登录后才能访问。

$this->middleware('signed')->only('verify');

设定了 只有 verify 动作使用 signed 中间件进行认证, signed 中间件是一种由框架提供的很方便的 URL 签名认证方式,此中间件的更多说明请见 Laravel 5.6 新功能 —— 路由签名 。

$this->middleware('throttle:6,1')->only('verify', 'resend');

对 verify 和 resend 动作做了频率限制,throttle 中间件是框架提供的访问频率限制功能,throttle 中间件会接收两个参数,这两个参数决定了在给定的分钟数内可以进行的最大请求数。 在这个例子中,我们限定了这两个动作访问频率是 1 分钟内不能超过 6 次。

3、VerifiesEmails Trait

控制器中:

use VerifiesEmails;

在 Laravel 的注册登录系统里面,一般都使用 PHP 的 Trait 机制来将提前设定好的功能注入到控制器里。在此控制器中,我们可以看到使用了 VerifiesEmails Trait ,打开此文件查看源码:

vendor/laravel/framework/src/Illuminate/Foundation/Auth/VerifiesEmails.php

<?php

namespace Illuminate\Foundation\Auth;

use Illuminate\Http\Request;
use Illuminate\Auth\Events\Verified;
use Illuminate\Auth\Access\AuthorizationException;

trait VerifiesEmails
{
    use RedirectsUsers;

    /**
     * 显示认证邮件提醒页面
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function show(Request $request)
    {
        return $request->user()->hasVerifiedEmail()
                        ? redirect($this->redirectPath())
                        : view('auth.verify');
    }

    /**
     * 处理认证成功后的业务逻辑,请注意签名认证发生在 `signed` 中间件里,
     * 在 VerificationController 的 __construct 方法里做了设定
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     * @throws \Illuminate\Auth\Access\AuthorizationException
     */
    public function verify(Request $request)
    {
        if ($request->route('id') != $request->user()->getKey()) {
            throw new AuthorizationException;
        }

        if ($request->user()->hasVerifiedEmail()) {
            return redirect($this->redirectPath());
        }

        if ($request->user()->markEmailAsVerified()) {
            event(new Verified($request->user()));
        }

        return redirect($this->redirectPath())->with('verified', true);
    }

    /**
     * 重新发送用户认证邮件
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function resend(Request $request)
    {
        if ($request->user()->hasVerifiedEmail()) {
            return redirect($this->redirectPath());
        }

        $request->user()->sendEmailVerificationNotification();

        return back()->with('resent', true);
    }
}

每个动作的作用上面代码中已做了注释,请注意看 verify 方法里这一段:

if ($request->user()->markEmailAsVerified()) {
    event(new Verified($request->user()));
}

如果用户能够成功设置为已认证的话,触发事件 Verified 并将用户传参。这里使用了 Laravel 的事件系统。

4、Laravel 事件系统

Laravel 事件是一套简单的观察者实现,能够订阅和监听应用中发生的各种事件。事件系统为应用各个方面的解耦提供了非常棒的解决方案,因为单个事件可以拥有多个互不依赖的监听器。

在我们这个场景中,用户认证成功后触发了 Verified 事件,我们对其进行监听即可加入我们想要的逻辑。此时也许有同学要问,为何不直接修改 vendor/laravel/framework/src/Illuminate/Foundation/Auth/VerifiesEmails.php 文件即可?因为此文件是 Laravel 框架自带的,本地修改后,无法纳入版本控制系统里,也无法同步到线上或者其他环境。所以正确的方式,是对 Verified 事件进行监听。

应用的事件监听需要在 EventServiceProvider 里注册:

app/Providers/EventServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Auth\Events\Verified;
use Illuminate\Support\Facades\Event;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
        Verified::class=>[
            \App\Listeners\EmailVerified::class,
        ],
    ];

    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        parent::boot();

        //
    }
}

这种键值对应的写法,可以让单个事件对应多个监听器,这里我们的事件是 \Illuminate\Auth\Events\Verified ,监听器是 \App\Listeners\EmailVerifiedListeners 文件夹是约定俗成的监听器命名,接下来我们使用命令行来生成此监听器:

$ php artisan event:generate

以上命令会为我们生成 app/Listeners/EmailVerified.php 文件,稍作修改:

<?php

namespace App\Listeners;

use Illuminate\Auth\Events\Verified;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;

class EmailVerified
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  Verified  $event
     * @return void
     */
    public function handle(Verified $event)
    {
        //会话里闪存认证成功后的消息提醒
        session()->flash('success','邮箱验证成功 ^_^')
    }
}

5、开始测试

事件监听器已经部署好,接下来我们测试一下。

为了测试方便,我们先手工更改数据,将用户还原到注册成功后的状态。打开数据库客户端,定位到当前登录用户,修改 email_verified_at 字段为 NULL

image.png

刷新页面会跳转至认证提醒页:

image.png

点击『点击重新发送 E-mail.』链接发送认证邮件:

image.png

打开 storage/logs 里的今日对应的 Log,定位到文件最尾端,取出验证链接。切换到浏览器测试下:

image.png


现在用户点击认证链接进入网站,如果认证成功,即可看到提示。

Git 代码版本控制

接着让我们将本次更改纳入版本控制中:

$ git add -A
$ git commit -m "认证后的消息提醒"

七、密码重置

1、找回密码

这节课我们来完善找回密码的逻辑。目前我们使用的是 make auth 生成的认证系统,原理是在控制器 ResetPasswordController 里使用 ResetsPasswords Trait 来集成框架功能。

本节课我们先来走一遍流程,看来会出现哪些问题,并对这些问题进行修复。

2、开始之前

本节课我们仍然使用 Summer 用户来做演示,演示之前我们需要激活一下,否则会被强制跳转到认证邮箱提醒页面。

进入 Tinker:

$ php artisan tinker

利用 markEmailAsVerified() 方法:

>>> App\Models\User::find(1)->markEmailAsVerified();=> true

确认一下 email_verified_at 是否不为 NULL

image.png

3、开始测试

点击导航栏里的登录按钮进入登录页面:

image.png


点击『忘记密码?』链接进入重试密码页面,写入邮箱:

image.png

提交:

image.png


此时系统会发送找回密码的邮件,因我们设置了邮件驱动为 Log ,打开 storage/logs 目录下对应今日的 Log 文件,并定位到文件尾部,找到重设密码的链接:

image.png

填写信息:

image.png


并提交:

image.png

发现了一个问题,直接跳转,没有消息提示,比较突兀,与邮件认证同样的问题。

4、分析源码

路由文件 web.php 中定义负责处理更改的动作是 ResetPasswordController 里的 reset() 方法:

Route::post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update');

打开 Auth\ResetPasswordController ,未发现 reset() 方法,根据我们之前的经验,应该在其加载的 Trait ResetsPasswords 里,打开此文件查看源码:

vendor/laravel/framework/src/Illuminate/Foundation/Auth/ResetsPasswords.php

public function reset(Request $request)
{
    $request->validate($this->rules(), $this->validationErrorMessages());

    // Here we will attempt to reset the user's password. If it is successful we
    // will update the password on an actual user model and persist it to the
    // database. Otherwise we will parse the error and return the response.
    $response = $this->broker()->reset(
        $this->credentials($request), function ($user, $password) {
            $this->resetPassword($user, $password);
        }
    );

    // If the password was successfully reset, we will redirect the user back to
    // the application's home authenticated view. If there is an error we can
    // redirect them back to where they came from with their error message.
    return $response == Password::PASSWORD_RESET
                ? $this->sendResetResponse($request, $response)
                : $this->sendResetFailedResponse($request, $response);
}


protected function sendResetResponse(Request $request, $response)
{
    return redirect($this->redirectPath())
                        ->with('status', trans($response));
}

从代码以及其中的注释中可看出,成功时候调用了 sendResetResponse() 方法,可惜此方法内的逻辑不是我们想要的。我们需要在表单提交成功后,设置闪存信息,再重定向到首页。

5、解决方案

我们可以利用 PHP 里 Trait 的加载机制,在控制器中重写 sendResetResponse() 方法:

app/Http/Controllers/Auth/ResetPasswordController.php

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;

class ResetPasswordController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Password Reset Controller
    |--------------------------------------------------------------------------
    |
    | This controller is responsible for handling password reset requests
    | and uses a simple trait to include this behavior. You're free to
    | explore this trait and override any methods you wish to tweak.
    |
    */

    use ResetsPasswords;

    /**
     * Where to redirect users after resetting their password.
     *
     * @var string
     */
    protected $redirectTo = '/';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest');
    }

    protected function sendResetResponse(Request $request, $response)
    {
        session()->flash('success','密码更新成功,您已成功登陆!');
        return redirect($this->redirectPath());
    }
}

重写 sendResetResponse() 的逻辑后,重新走一遍上面的找回密码的流程。当新密码表单提交后,即可看到我们温馨的消息提醒:

image.png

Git 代码版本控制

接着让我们将本次更改纳入版本控制中:

$ git add -A
$ git commit -m "找回密码成功消息提示"

八、小结

总结

在本章节中,我们学习了:

  • 用户注册登录功能的开发;

  • 学会查看 Whoops 报错和定位问题;

  • 添加注册验证码;

  • 数据库视图工具查看 Homestead 中的数据库;

  • 中间件的使用;

  • Laravel 事件系统初识;

  • 验证错误消息中文化;

  • 邮箱认证;

  • 找回密码;

  • 重写 Trait 里的方法。

同步代码

接下来我们需要将代码推送至服务器:

$ git push origin master


回复列表



回复操作

正在加载验证码......

请先拖动验证码到相应位置