Menu

71.邮箱认证(一)

本节说明

  • 对应视频教程第 71 小节:Users Must Confirm Their Email Address: #1 - Protection

本节内容

我们继续开发下一个功能:邮箱认证。按照惯例,我们新建测试:
forum\tests\Feature\CreateThreadsTest.php

    .
    .
    /** @test */
    public function guests_may_not_create_threads()
    {
        $this->withExceptionHandling();

        $this->get('/threads/create')
            ->assertRedirect('/login');

        $this->post('/threads')
            ->assertRedirect('/login');
    }

    /** @test */
    public function authenticated_users_must_first_confirm_their_email_address_before_creating_threads()
    {
        $this->publishThread()
            ->assertRedirect('/threads')
            ->assertSessionHas('flash','You must first confirm your email address.');
    }
    .
    .

接下来我们修改迁移文件,为users表增加一个confirmed字段:
forum\database\migrations\2014_10_12_000000_create_users_table.php

    .
    .
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->string('avatar_path')->nullable();
            $table->boolean('confirmed')->default(false);
            $table->rememberToken();
            $table->timestamps();
        });
    }
    .
    .

运行迁移:

$ php artisan migrate:refresh

进入Tinker:

$ php artisan tinker

填充数据:

>>> factory('App\Thread',30)->create();

为了方便测试,我们直接在模型工厂文件中给confirmed字段返回false
forum\database\factories\ModelFactory.php

.
.
$factory->define(App\User::class, function (Faker\Generator $faker) {
    static $password;

    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'password' => $password ?: $password = bcrypt('123456'),
        'remember_token' => str_random(10),
        'confirmed' => false,
    ];
});
.
.

我们接下来修改控制器使测试通过:
forum\app\Http\Controllers\ThreadsController.php

    .
    .
    public function store(Request $request)
    {
        if (! auth()->user()->confirmed) {
            return redirect('/threads')->with('flash','You must first confirm your email address.');
        }

        $this->validate($request,[
           'title' => 'required|spamfree',
            'body' => 'required|spamfree',
            'channel_id' => 'required|exists:channels,id'
        ]);

        $thread = Thread::create([
            'user_id' => auth()->id(),
            'channel_id' => request('channel_id'),
            'title' => request('title'),
            'body' => request('body'),
        ]);

        return redirect($thread->path())
            ->with('flash','Your thread has been published!');
    }
    .
    .

运行测试:
file

接下来我们将利用 Laravel 中间件 功能来过滤掉未认证用户发表话题的请求。

本站教程内容引用

我们引用本站第二本教程 L02 Laravel 教程 - Web 开发实战进阶 ( Laravel 5.5 ) 对中间件的说明:

Laravel 中间件提供了一种方便的机制来过滤进入应用的 HTTP 请求。例如,Laravel 内置了一个中间件来验证用户的身份认证。如果用户没有通过身份认证,中间件会将用户重定向到登录界面。但是,如果用户被认证,中间件将允许该请求进一步进入该应用。

当然,除了身份认证以外,中间件还可以用来执行各种任务。例如:CORS 中间件可以负责为所有离开应用的响应添加合适的头部信息;日志中间件可以记录所有传入应用的请求。Laravel 自带了一些中间件,包括身份验证、CSRF 保护等。所有这些中间件都位于 app/Http/Middleware 目录中。

Laravel 的中间件从执行时机上分『前置中间件』和『后置中间件』,前置中间件是应用初始化完成以后立刻执行,此时控制器路由还未分配、控制器还未执行、视图还未渲染。后置中间件是即将离开应用的响应,此时控制器已将渲染好的视图返回,我们可以在后置中间件里修改响应。两者的区别在于书写方式的不同:

前置中间件:

<?php

namespace App\Http\Middleware;

use Closure;

class BeforeMiddleware
{
    public function handle($request, Closure $next)
    {
        // 这是前置中间件,在还未进入 $next 之前调用

        return $next($request);
    }
}

后置中间件:

<?php

namespace App\Http\Middleware;

use Closure;

class AfterMiddleware
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        // 这是后置中间件,$next 已经执行完毕并返回响应 $response,
        // 我们可以在此处对响应进行修改。

        return $response;
    }
}

注意他们的区别在于 $next($request) 的执行位置,而非类的命名或者其他。

创建中间件

运行以下命令,生成中间件类文件:

$ php artisan make:middleware RedirectIfEmailNotConfirmed

注册中间件

想让中间件在应用的每个 HTTP 请求期间运行,我们还需要在 app/Http/Kernel.php 类中对中间件进行注册。
forum\app\Http\Kernel.php

<?php

namespace App\Http;

use App\Http\Middleware\RedirectIfEmailNotConfirmed;
use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    .
    .
    protected $routeMiddleware = [
        'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'must-be-confirmed' => RedirectIfEmailNotConfirmed::class
    ];
}

书写中间件类

forum\app\Http\Middleware\RedirectIfEmailNotConfirmed.php

<?php

namespace App\Http\Middleware;

use Closure;

class RedirectIfEmailNotConfirmed
{
    public function handle($request, Closure $next)
    {
        if (! $request->user()->confirmed) { // 如果用户未认证,则重定向
            return redirect('/threads')->with('flash','You must first confirm your email address.');
        }

        return $next($request);
    }
}

应用中间件

我们删除控制器的代码片段:
forum\app\Http\Controllers\ThreadsController.php

    .
    .
    public function store(Request $request)
    {
        $this->validate($request,[
           'title' => 'required|spamfree',
            'body' => 'required|spamfree',
            'channel_id' => 'required|exists:channels,id'
        ]);

        $thread = Thread::create([
            'user_id' => auth()->id(),
            'channel_id' => request('channel_id'),
            'title' => request('title'),
            'body' => request('body'),
        ]);

        return redirect($thread->path())
            ->with('flash','Your thread has been published!');
    }
    .
    .

应用中间件:
forum\routes\web.php

.
.
Route::post('threads','ThreadsController@store')->middleware('must-be-confirmed');
.
.

再次运行测试:
file
测试通过,但是我们还有一个小问题需要注意下:如果我们现在用一个未认证用户尝试发布话题,的确我们会被重定向至话题列表页面,但是我们存入session的消息没有显示出来。我们期望显示消息,所以我们需要修改Flash组件:
forum\resources\assets\js\components\Flash.vue

<template>
    <div class="alert alert-flash"
         :class="'alert-'+level"
         role="alert"
         v-show="show"
         v-text="body">
    </div>
 </template>

<script>
    export default {
        props:['message'],

        data(){
            return {
                body : this.message, // 此处赋予初始值
                level : 'success',
                show:false
            }
        },

        created(){
            if(this.message){
                this.flash(); // 更改逻辑为:如果有消息,则展示
            }

            window.events.$on(
                'flash',data => this.flash(data)
            );
        },

        methods:{
            flash(data){
                // 修改处理方式:如果传入了对象参数,则重写 body,level
                if(data) {
                    this.body = data.message;
                    this.level = data.level;
                }

                this.show = true;

                this.hide();
            },

            hide(){
                setTimeout( () => {
                   this.show = false;
                },3000);
            }
        }
    };
</script>

<style>
    .alert-flash{
        position: fixed;
        right: 25px;
        bottom: 25px;
    }
</style>

现在我们再来尝试:
file

本文章首发在 Laravel China 社区
上一篇 下一篇
讨论数量: 0
发起讨论


暂无话题~