E-mail подтверждение регистрации пользователя в Laravel

Суть проекта такова: пользователь заполняет форму регистрации, после чего пользовательские данные заносятся в БД. Но сразу после этого пользователь не сможет зайти под своим именем на сайт, так как сначала он должен пройти проверку подлинности своего почтового ящика. Для осуществлении такой проверки, кроме стандартных полей в таблицу users (name, email, password) мы добавим 2 дполнительных поля: verified и token. Первое поле может принимать значения true/false (изначально false), второе поле служит для хранения случайного значения – ключа. На свой почтовый ящик пользователь получает сообщение со ссылкой, содержащей этот ключ из базы. После того как пользователь переходит по ссылке, ключ из ссылки и ключ из базы сравниваются, при их совпадении поле verified переходит в состояние true, а ключ удаляется. Активация считается завершенной. После этого пользователь сможет успешно пройти аутентификацию.

С использованием собственной аутентификации

Для работы с БД нам потребуется изменить стандартный файл миграции 2014_10_12_000000_create_users_table.php, добавим в него необходимые поля: verified и token. После чего содержимое метода up() будет таким:

Schema::create('users', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
    $table->string('email')->unique();
    $table->string('password', 60);
    $table->boolean('verified')->default(false);
    $table->string('token')->nullable();
    $table->rememberToken();
    $table->timestamps();
});

Перейдем в файл маршрутов (web.php). Создадим:

  1. маршруты (post и get) для регистрации и авторизации;
  2. маршрут для проверки ключа, полученного пользователем в письме, отправленном ему после регистрации;
  3. маршрут для выхода (logout) со страницы администратора (dashboard) и для входа на нее.

Готовый файл будет таким:

Route::get('/', function () {
    return view('welcome');
});
Route::get('register', 'RegistrationController@register');
Route::post('register', 'RegistrationController@postRegister');
Route::get('register/confirm/{token}', 'RegistrationController@confirmEmail');
Route::get('login', 'SessionsController@login')->middleware('guest');
Route::post('login', 'SessionsController@postLogin')->middleware('guest');
Route::get('logout', 'SessionsController@logout');
Route::get('dashboard', ['middleware' => 'auth', function() {
    return 'Добро пожаловать, '.Auth::user()->name.'!';
}]); 

Далее создадим первый контроллер RegistrationController. В него добавим метод confirmEmail(), который подтверждает введенный пользователем почтовый адрес:

public function confirmEmail(Request $request, $token)
{
    User::whereToken($token)->firstOrFail()->confirmEmail();
    $request->session()->flash('message', 'Учетная запись подтверждена. Войдите под своим именем.');
    return redirect('login');
}

Как вы могли заметить по файлу маршрутов, этот контроллер содержит еще два метода: register() и postRegister(). Первый отвечает за генерацию формы регистрации:

public function register()
{
    return view('auth.register');
}

Второй за проверку данных из этой формы, заполненной пользователем:

public function postRegister(Request $request)
{
    $this->validate($request, [
        'name' => 'required',
        'email' => 'required|email|unique:users',
        'password' => 'required'
    ]);
    $user = User::create($request->all());
    Mail::to($user)->send(new UserRegistered($user));
    $request->session()->flash('message', 'На ваш адрес было выслано письмо с подтверждением регистрации.');
    return redirect()->back();
}

В конструкторе контроллера вызовем middleware:

public function __construct()
{
    $this->middleware('guest');
}

Также не забудьте в заголовке контроллера добавить нужные строчки для классов UserRegistered и Mail. В результате чего заголовок должен выглядеть следующим образом:

namespace App\Http\Controllers;
use App\User;
use App\Mail\UserRegistered;
use Illuminate\Support\Facades\Mail;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

Второй из нужных нам контроллеров, это SessionsController. В нем создадим 5 методов. Первый метод – это метод с формой для входа:

public function login()
{
    return view('auth.login');
}

Второй для обработки данных с этой формы:

public function postLogin(Request $request)
{
    $this->validate($request, ['email' => 'required|email', 'password' => 'required']);
    if ($this->signIn($request)) {
        $request->session()->flash('message', 'Добро пожаловать!');
        return redirect()->intended('dashboard');
    }
    $request->session()->flash('message', 'Вход запрещен!');
    return redirect()->back();
}

Третий для выхода (logout):

public function logout(Request $request)
{
    Auth::logout();
    $request->session()->flash('message', 'Вы разлогинились.');
    return redirect('login');
}

Четвертый и пятый для проверки данных из формы (используются во втором методе):

protected function signIn(Request $request)
{
    return Auth::attempt($this->getCredentials($request), $request->has('remember'));
}
protected function getCredentials(Request $request)
{
    return [
        'email'    => $request->input('email'),
        'password' => $request->input('password'),
        'verified' => true
    ];
}

В первом контроллере RegistrationController есть строчка:

Mail::to($user)->send(new UserRegistered($user));

С помощью нее мы отправляем письмо пользователю после заполнения формы регистрации. Начиная с версии 5.3 для отправки писем можно воспользоваться классом Mailable. Подробнее об отправке писем можно почитать в документации, в разделе Mail. Чтобы создать свой mailable-класс (в нашем случае класс с именем UserRegistered) нужно выполнить команду:

php artisan make:mail UserRegistered

После чего в директории проекта появится новая папка Mail, а в ней файл UserRegistered.php, с mailable-классом. Для нашего проекта содержимое класса UserRegistered будет следующим:

use Queueable, SerializesModels;
public $user;
public function __construct(User $user)
{
    $this->user = $user;
}
public function build()
{
    return $this->from('[email protected]')
            ->view('emails.confirm');
}

В методе build() мы используем вид confirm.blade.php, в котором содержится структура письма. Структура может быть такая:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Активация регистрации нового ползователя</title>
</head>
<body>
    <h1>Спасибо за регистрацию, {{$user->name}}!</h1>
    <p>
        Перейдите <a href='{{ url("register/confirm/{$user->token}") }}'>по ссылке </a>чтобы завершить регистрацию!
    </p>
</body>
</html>
На этапе разработки для получения писем можно воспользоваться файлом laravel.log. Для этого в файле .env в строчке MAIL_DRIVER нужно указать log.

Переходим к модели User. В модели нам потребуется три метода. Первый – это boot()-метод, который выполняется при загрузке модели. С помощью него мы создаем ключ подтверждения, отправляемый пользователю. Необходимый метод будет выглядеть следующим образом:

public static function boot()
{
    parent::boot();
    static::creating(function ($user) {
        $user->token = str_random(30);
    });
}

Во втором методе мы хэшируем пароль пользователя:

public function setPasswordAttribute($password)
{
    $this->attributes['password'] = bcrypt($password);
}

В третьем методе, при успешной валидации пользователя мы удаляем ключ из базы и задаем статус поля verified равным true:

public function confirmEmail()
{
    $this->verified = true;
    $this->token = null;
    $this->save();
}

Осталось создать виды. Напоминаю, что для письма вид уже был создан. Основной вид приложения (используется Bootstrap):

<html>
<head>
    <meta charset="UTF-8">
    <title>E-mail активация</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>
    <div class="container">
        @include('flash')
        @yield('content')
    </div>
</body>
</html>

В этом виде используется вложенный вид flash для вывода сообщений:

@if (session()->has('message'))
    <div class="alert alert-info">{{ session('message') }}</div>
@endif

Вид с формой для регистрации:

@extends('app')
@section('content')
<h1>Регистрация</h1>
@include('errors')
<form method="POST" action='{{ url("register/") }}'>
    {!! csrf_field() !!}
    <div class="form-group">
        <label for="name">Имя:</label>
        <input type="text" name="name" id="name" class="form-control" value="{{ old('name') }}">
    </div>
    <div class="form-group">
        <label for="email">Email:</label>
        <input type="text" name="email" id="email" class="form-control" value="{{ old('email') }}">
    </div>
    <div class="form-group">
        <label for="password">Пароль:</label>
        <input type="password" name="password" id="password" class="form-control">
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-default">Зарегистрироваться</button>
    </div>
    </form>
@stop

Вид с формой для входа на сайт:

@extends('app')
@section('content')
    <h1>Авторизация</h1>
    @include('errors')
    <form method="POST" action='{{ url("login/") }}'>
        {{csrf_field()}}
        <div class="form-group">
            <label for="email">Email:</label>
            <input type="text" name="email" id="email" class="form-control" value="{{ old('email') }}">
        </div>
        <div class="form-group">
            <label for="password">Пароль:</label>
            <input type="password" name="password" id="password" class="form-control">
        </div>
        <div class="checkbox">
            <label>
                <input type="checkbox" name="remember" id="remember" {{ old('remember') ? ' checked' : '' }}> Запомнить меня
            </label>
        </div>
        <div class="form-group">
            <button type="submit" class="btn btn-default">Войти</button>
        </div>
    </form>
@stop

Оба вида (регистрации и авторизации) используют вложенный вид errors:

@if (count($errors) > 0)
    <div class="alert alert-danger">
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

С использованием аутентификации из коробки

Для тестирования кода использовался Laravel 5.5, но я думаю подойдут и версии 5.3 или 5.4. 
Так как здесь мы используем аутентификацию из коробки, то ее нужно сначала установить, делается это с помощью команды:
php artisan make:auth
В результате чего вы получите, готовую к использованию, систему регистрации и авторизации пользователей, а также набор необходимых контроллеров и видов для возможной ее кастомизации. 
В данном примере мы это и сделаем, кастомизируем предлагаемый функционал регистрации и авторизации, и добавим в него возможность подтверждения регистрации по e-mail. Для этого, как было сказано вначале, нам нужно добавить 2 дополнительных поля в таблицу users (verified и token). У нас уже есть стандартный файл миграции create_users_table.php, можно добавить 2 новых поля прямо в него, но у кого-то миграция уже может быть проведена и таблицы в БД уже созданы, поэтому удобнее было бы просто добавить 2 новых поля к существующей таблице. Так мы и поступим. 
Для добавления двух новых полей к таблице нам нужно создать дополнительный файл миграции, который это делает. Для его создания потребуется следующая команда:
php artisan make:migration add_verified_token_fields_to_users_table —table=users
После этого в папке migrations появится новый файл. Откроем его и в метод up() добавим следующее содержимое:
Schema::table('users', function (Blueprint $table) {
    $table->boolean('verified')->default(false);
    $table->string('token')->nullable();
});
Теперь осталось провести миграцию командой:
php artisan migrate
С БД закончили, теперь переходим к добавлению нового функционала. 
По традиции начнем с маршрутов. Откроем файл web.php и добавим в него маршрут, по которому будет осуществляться сверка ключа (token) из пользовательской ссылки и ключа в БД.
Route::get('register/confirm/{token}', 'Auth\RegisterController@confirmEmail');
Как видим за данную проверку должен отвечать метод confirmEmail() контроллера Auth\RegisterController. Сам контроллер у нас уже есть, поэтому откроем его и добавим необходимый метод. Выглядеть он должен следующим образом:
public function confirmEmail(Request $request, $token)
{
    User::whereToken($token)->firstOrFail()->confirmEmail();
    $request->session()->flash('message', 'Учетная запись подтверждена. Войдите под своим именем.');
    return redirect('login');
}
Продолжим работу с контроллером. В заголовке добавим следующие строчки:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use App\Mail\UserRegistered;
В последней строчке UserRegistered – это mailable-класс, который будет отвечать за отправку писем. Мы создадим его позже, потому что в контроллер нам нужно добавить еще один метод. Это метод register(), отвечающий за сам процесс регистрации. Выглядеть он должен следующим образом:
public function register(Request $request)
{
    $this->validator($request->all())->validate();
    $user = $this->create($request->all());
    Mail::to($user)->send(new UserRegistered($user));
    $request->session()->flash('message', 'На ваш адрес было выслано письмо с подтверждением регистрации.');
    return back();
}
Здесь мы создаем новую запись в таблице users и отправляем письмо-подтверждение регистрации. Важный момент в том, что при создании новой записи туда еще нужно добавить случайное значение в поле token. Делается это с помощью метода boot() модели User. Откроем модель и добавим туда этот метод.
public static function boot()
{
    parent::boot();
    static::creating(function ($user) {
        $user->token = str_random(30);
    });
}
Также в модель мы должны добавить метод, который будет обнулять ключ и устанавливать значение поля verified равным true, в случае, если пользователь успешно подтвердит свой e-mail адрес.
public function confirmEmail()
{
    $this->verified = true;
    $this->token = null;
    $this->save();
}
С моделью закончили. Теперь нам нужно создать mailable-класс отправки письма UserRegistered, который мы используем в контроллере Auth\RegisterController. Для этого воспользуемся следующей командой:
php artisan make:mail UserRegistered
После чего в директории проекта появится новая папка Mail, а в ней файл UserRegistered.php, с mailable-классом. Для нашего проекта содержимое класса UserRegistered будет следующим:
use Queueable, SerializesModels;
public $user;
public function __construct(User $user)
{
    $this->user = $user;
}
public function build()
{
    return $this->to($this->user->email)
            ->view('auth.emails.confirm');
}
В методе build() мы используем вид confirm.blade.php (папка /resources/views/auth/emails), в котором содержится структура письма. Структура может быть такая:
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Активация регистрации нового ползователя</title>
</head>
<body>
    <h1>Спасибо за регистрацию, {{$user->name}}!</h1>
    <p>
        Перейдите <a href='{{ url("register/confirm/{$user->token}") }}'>по ссылке </a>чтобы завершить регистрацию!
    </p>
</body>
</html>
На этапе разработки для получения писем можно воспользоваться файлом laravel.log. Для этого в файле .env в строчке MAIL_DRIVER нужно указать log.
На данный момент мы сделали все, чтобы пользователь мог зарегистрироваться и пройти проверку подлинности, указанного во время регистрации почтового ящика. Но остался важный момент – изменить функционал предустановленной авторизации, для того чтобы на сайт могли входить только пользователи, прошедшие e-mail-проверку, то есть те пользователи, у которых поле verified равно true. 
Для этого откроем уже существующий Auth\LoginController. В заголовок класса добавим строчку:
use Illuminate\Http\Request;
И добавим 2 новых метода: attemptLogin() и credentials(). Второй метод вспомогательный, то есть используется в первом. Оба метода выполняют аутентификацию пользователя, если поле verified у пользователя равно true.
protected function attemptLogin(Request $request)
{
    return $this->guard()->attempt(
        $this->credentials($request), $request->filled('remember')
    );
}
protected function credentials(Request $request)
{
    return array_add($request->only($this->username(), 'password'), 'verified', true);
}
Основную часть работы мы сделали, осталось добавить в виды register.blade.php и login.blade.php вывод сообщений о том, что письмо было выслано, и что учетная запись была подтверждена. 
Код отвечающий за вывод сообщения одинаковый для обоих видов:
@if (session()->has('message'))
    <div class="alert alert-info">{{ session('message') }}</div>
@endif
После этого страница регистрации будет выглядеть так:Страница аутентификации так: