Магазин на Laravel 7, часть 8. Регистрация и аутентификация пользователей на сайте

08.10.2020

Теги: LaravelMySQLPHPWeb-разработкаАутентификацияБазаДанныхИнтернетМагазинКаталогТоваровПользовательПрактикаФреймворкШаблонСайта

В Laravel сделать аутентификацию очень просто — почти всё готово из коробки. Мы уже установили ранее пакет laravel/ui, чтобы использовать в шаблонах фреймворк bootstrap. Для создания заготовок всех необходимых для аутентификации контроллеров, шаблонов и роутов нужно выполнить artisan-команду.

> php artisan ui:auth
> npm install && npm run dev

Будут созданы контроллеры:

  • RegisterController — обеспечивает регистрацию пользователей
  • LoginController — обеспечивает аутентификацию пользователей
  • ForgotPasswordController — отправляет письмо на сброс пароля
  • ResetPasswordController — содержит логику для сброса паролей

Будут созданы шаблоны:

  • auth.register — форма регистрации пользователей
  • auth.login — форма аутентификации пользователей
  • auth.passwords.email — форма для ввода адреса почты (восстановление пароля)
  • auth.passwords.reset — форма для ввода нового пароля (восстановление пароля)

Будут добавлены маршруты:

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

Вызов Auth::routes() добавляет сразу около дюжины маршрутов, посмотреть эти маршруты можно в классе Laravel\Ui\AuthRouteMethods. Но мы изменим все имена маршрутов и добавим префикс, чтобы они начинались с user. По умолчанию страница регистрации доступна по адресу /register, а будет доступна по адресу /user/register. По умолчанию имя маршрута регистрации register, а у нас имя будет user.register.

Route::name('user.')->prefix('user')->group(function () {
    Auth::routes();
});
Route::get('/home', 'HomeController@index')->name('home');

Кроме контроллеров, перечисленных выше, будет добавлен контроллер HomeController, отвечающий за показ страницы /home. После регистрации и аутентификации пользователи перенаправляются на эту страницу. Нам этот контроллер не нужен, так что можно его удалить. Также можно удалить шаблон home.blade.php и layout-шаблон app.blade.php — но пока не будем этого делать, нам нужно кое-что забрать оттуда.

Сейчас пользователи уже могут регистрироваться и аутентифицироваться. Если открыть в браузере страницу /user/register, то будет показана форма регистрации нового пользователя.

Но не хватает перевода на русский язык. Чтобы получить языковые файлы, установим пакет

> composer require laravel-lang/lang:~7.0

После этого нужно скопировать директорию vendor/laravel-lang/lang/src/ru и файл vendor/laravel-lang/lang/json/ru.json в директорию resources/lang. И проверяем форму регистрации пользователей еще раз.

Теперь посмотрим, что находится в layout-шаблоне app.blade.php. Нас интересует следующий фрагмент кода:

<!-- Right Side Of Navbar -->
<ul class="navbar-nav ml-auto">
    <!-- Authentication Links -->
    @guest
        <li class="nav-item">
            <a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
        </li>
        @if (Route::has('register'))
            <li class="nav-item">
                <a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a>
            </li>
        @endif
    @else
        <li class="nav-item dropdown">
            <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown"
               aria-haspopup="true" aria-expanded="false" v-pre>
                {{ Auth::user()->name }}
            </a>
            <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                <a class="dropdown-item" href="{{ route('logout') }}"
                   onclick="event.preventDefault();
                                 document.getElementById('logout-form').submit();">
                    {{ __('Logout') }}
                </a>
                <form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
                    @csrf
                </form>
            </div>
        </li>
    @endguest
</ul>

Возьмем отсюда все полезное и вставим в наш layout-шаблон site.balde.php:

<!-- Этот блок расположен справа -->
<ul class="navbar-nav ml-auto">
    <li class="nav-item">
        <a class="nav-link" href="{{ route('basket.index') }}">Корзина</a>
    </li>
    @guest
        <li class="nav-item">
            <a class="nav-link" href="{{ route('user.login') }}">Войти</a>
        </li>
        @if (Route::has('user.register'))
            <li class="nav-item">
                <a class="nav-link" href="{{ route('user.register') }}">Регистрация</a>
            </li>
        @endif
    @else
        <li class="nav-item">
            <a class="nav-link" href="{{ route('user.index') }}">Личный кабинет</a>
        </li>
    @endif
</ul>

Давайте добавим flash-сообщения, которые будут показываться при регистрации, входе в личный кабинет и при выходе:

class RegisterController extends Controller {
    /* ... */

    /**
     * Сразу после регистрации выполняем редирект и устанавливаем flash-сообщение
     */
    protected function registered(Request $request, $user) {
        return redirect()->route('user.index')
            ->with('success', 'Регистрация на сайте прошла успешно');
    }
}
class LoginController extends Controller {
    /* ... */

    /**
     * Сразу после входа выполняем редирект и устанавливаем flash-сообщение
     */
    protected function authenticated(Request $request, $user) {
        return redirect()->route('user.index')
            ->with('success', 'Вы успешно вошли в личный кабинет');
    }

    /**
     * Сразу после выхода выполняем редирект и устанавливаем flash-сообщение
     */
    protected function loggedOut(Request $request) {
        return redirect()->route('user.login')
            ->with('success', 'Вы успешно вышли из личного кабинета');
    }
}

Добавленные в контроллерах flash-сообщения будем показываем в layout-шаблоне, так что добавим в него следующий код:

<div class="row">
    <div class="col-md-3">
        @include('layout.part.roots')
        @include('layout.part.brands')
    </div>
    <div class="col-md-9">
        @if ($message = Session::get('success'))
            <div class="alert alert-success alert-dismissible mt-4" role="alert">
                <button type="button" class="close" data-dismiss="alert" aria-label="Закрыть">
                    <span aria-hidden="true">&times;</span>
                </button>
                {{ $message }}
            </div>
        @endif
        @yield('content')
    </div>
</div>

Теперь давайте исправим шаблоны страниц регистрации, аутентификации и восстановления пароля, чтобы шапка сайта была как на всех прочих страницах. Для этого достаточно изменить директиву @extends() в шаблонах директории views/auth, чтобы подключался наш layout-шаблон.

@extends('layout.site')

@section('content')
<div class="row">
    <div class="col-12">
    <!-- здесь форма регистрации -->
    </div>
</div>
@endsection

@extends('layout.site')

@section('content')
<div class="row">
    <div class="col-12">
    <!-- здесь форма аутентификации -->
    </div>
</div>
@endsection
@extends('layout.site')

@section('content')
<div class="row">
    <div class="col-12">
    <!-- здесь форма ввода адреса почты (восстановление пароля) -->
    </div>
</div>
@endsection
@extends('layout.site')

@section('content')
<div class="row">
    <div class="col-12">
    <!-- здесь форма ввода нового пароля (восстановление пароля) -->
    </div>
</div>
@endsection

Кроме того, надо везде в шаблонах заменить имена роутов в хелпере route(), потому что мы изменили ранее имена register, login, logout на user.register, user.login, user.logout.

И последнее, что осталось сделать — создать страницу, куда пользователь будет попадать сразу после регистрации или аутентификации. Добавим маршрут user.index, создадим контроллер UserController и шаблон index.blade.php в директории views/user.

Route::name('user.')->prefix('user')->group(function () {
    Route::get('index', 'UserController@index')->name('index');
    Auth::routes();
});

Контроллер UserController удобнее всего сделать из HomeController — все экшены уже защищены auth-посредником.

namespace App\Http\Controllers;

use Illuminate\Http\Request;

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

    /**
     * Show the application dashboard.
     *
     * @return \Illuminate\Contracts\Support\Renderable
     */
    public function index() {
        return view('user.index');
    }
}
@extends('layout.site')

@section('content')
    <h1>Личный кабинет</h1>
    <p>Добро пожаловать, {{ auth()->user()->name }}</p>
    <p>Это личный кабинет постоянного покупателя нашего интернет-магазина.</p>
    <form action="{{ route('user.logout') }}" method="post">
        @csrf
        <button type="submit" class="btn btn-primary">Выйти</button>
    </form>
@endsection

На страницу /user/index пользователь может попасть только в том случае, если он вошел в личный кабинет. Если же нет — мы его отправляем на страницу аутентификации. Это можно сделать в классе посредника Authenticate.

namespace App\Http\Middleware;

use Illuminate\Auth\Middleware\Authenticate as Middleware;

class Authenticate extends Middleware {
    /**
     * Get the path the user should be redirected to when they are not authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return string|null
     */
    protected function redirectTo($request) {
        if (! $request->expectsJson()) {
            return route('user.login');
        }
    }
}

Для панели управления нам потребуется несколько контроллеров. Неудобно в конструкторе каждого из них прописывать два посредника auth и admin. Давайте вообще уберем конструктор в контроллере Admin\IndexController, а посредники пропишем в маршруте.

// первый способ добавления посредников
Route::namespace('Admin')->name('admin.')->prefix('admin')->middleware('auth', 'admin')->group(function () {
    Route::get('index', 'IndexController')->name('index');
});
// второй способ добавления посредников
Route::group([
    'as' => 'admin.', // имя маршрута, например admin.index
    'prefix' => 'admin', // префикс маршрута, например admin/index
    'namespace' => 'Admin', // пространство имен контроллера
    'middleware' => ['auth', 'admin'] // один или несколько посредников
], function () {
    Route::get('index', 'IndexController')->name('index');
});

Post Scriptum

Много позже возникла проблема с восстановлением пароля. При отправке формы с адресом почты, чтобы получить ссылку для восстанвления пароля, вылезала ошибка «Route [password.reset] not defined». Видимо, где-то намудрил с маршрутами, хотя вроде бы ничего особо не менял, за исключением добавления префикса user. Но как раз с префиксом и возникли проблемы — вместо user.password.reset происходит обращение к password.reset. Маршруты для регистрации, аутентификации, восстановления пароля определены в классе Laravel\Ui\AuthRouteMethods.

/*
 * Регистрация, вход в ЛК, восстановление пароля
 */
Route::name('user.')->prefix('user')->group(function () {
    Auth::routes();
});
namespace Laravel\Ui;

class AuthRouteMethods
{
    /**
     * Register the typical authentication routes for an application.
     *
     * @param  array  $options
     * @return callable
     */
    public function auth()
    {
        return function ($options = []) {
            // Login Routes...
            if ($options['login'] ?? true) {
                $this->get('login', 'Auth\LoginController@showLoginForm')->name('login');
                $this->post('login', 'Auth\LoginController@login');
            }
            
            // Logout Routes...
            if ($options['logout'] ?? true) {
                $this->post('logout', 'Auth\LoginController@logout')->name('logout');
            }

            // Registration Routes...
            if ($options['register'] ?? true) {
                $this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
                $this->post('register', 'Auth\RegisterController@register');
            }

            // Password Reset Routes...
            if ($options['reset'] ?? true) {
                $this->resetPassword();
            }

            // Password Confirmation Routes...
            if ($options['confirm'] ??
                class_exists($this->prependGroupNamespace('Auth\ConfirmPasswordController'))) {
                $this->confirmPassword();
            }

            // Email Verification Routes...
            if ($options['verify'] ?? false) {
                $this->emailVerification();
            }
        };
    }

    /**
     * Register the typical reset password routes for an application.
     *
     * @return void
     */
    public function resetPassword()
    {
        return function () {
            $this->get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
            $this->post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
            $this->get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
            $this->post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update');
        };
    }

    /**
     * Register the typical confirm password routes for an application.
     *
     * @return void
     */
    public function confirmPassword()
    {
        return function () {
            $this->get('password/confirm', 'Auth\ConfirmPasswordController@showConfirmForm')->name('password.confirm');
            $this->post('password/confirm', 'Auth\ConfirmPasswordController@confirm');
        };
    }

    /**
     * Register the typical email verification routes for an application.
     *
     * @return void
     */
    public function emailVerification()
    {
        return function () {
            $this->get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
            $this->get('email/verify/{id}/{hash}', 'Auth\VerificationController@verify')->name('verification.verify');
            $this->post('email/resend', 'Auth\VerificationController@resend')->name('verification.resend');
        };
    }
}
  • Маршрут user.password.request отвечает за показ формы ввода адреса почты, куда будет отправлена ссылка
  • Маршрут user.password.email отвечает за отправку письма, содержащего ссылку для восстановления пароля
  • Маршрут user.password.reset отвечает за показ формы сброса пароля, где можно ввести новый пароль
  • Маршрут user.password.update отвечает за обновление пароля в базе данных после отправки формы

Проверил шаблоны, что в них указаны правильные маршруты — вроде все в порядке. Пришлось копать глубже — не углубляясь в детали, класс модели User расширяет класс Illuminate\Foundation\Auth\User. А этот класс в свою очередь использует трейты

namespace Illuminate\Foundation\Auth;

use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\MustVerifyEmail;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\Access\Authorizable;

class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract {
    use Authenticatable, Authorizable, CanResetPassword, MustVerifyEmail;
}

Нас интересует трейт Illuminate\Auth\Passwords\CanResetPassword:

namespace Illuminate\Auth\Passwords;

use Illuminate\Auth\Notifications\ResetPassword as ResetPasswordNotification;

trait CanResetPassword {
    /**
     * Get the e-mail address where password reset links are sent.
     *
     * @return string
     */
    public function getEmailForPasswordReset()
    {
        return $this->email;
    }

    /**
     * Send the password reset notification.
     *
     * @param  string  $token
     * @return void
     */
    public function sendPasswordResetNotification($token)
    {
        $this->notify(new ResetPasswordNotification($token));
    }
}

Смотрим класс Illuminate\Auth\Notifications\ResetPassword:

namespace Illuminate\Auth\Notifications;

use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Lang;

class ResetPassword extends Notification {
    /**
     * The password reset token.
     *
     * @var string
     */
    public $token;

    /**
     * The callback that should be used to create the reset password URL.
     *
     * @var \Closure|null
     */
    public static $createUrlCallback;

    /**
     * The callback that should be used to build the mail message.
     *
     * @var \Closure|null
     */
    public static $toMailCallback;

    /**
     * Create a notification instance.
     *
     * @param  string  $token
     * @return void
     */
    public function __construct($token)
    {
        $this->token = $token;
    }

    /**
     * Get the notification's channels.
     *
     * @param  mixed  $notifiable
     * @return array|string
     */
    public function via($notifiable)
    {
        return ['mail'];
    }

    /**
     * Build the mail representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return \Illuminate\Notifications\Messages\MailMessage
     */
    public function toMail($notifiable)
    {
        if (static::$toMailCallback) {
            return call_user_func(static::$toMailCallback, $notifiable, $this->token);
        }

        if (static::$createUrlCallback) {
            $url = call_user_func(static::$createUrlCallback, $notifiable, $this->token);
        } else {
            $url = url(route('password.reset', [
                'token' => $this->token,
                'email' => $notifiable->getEmailForPasswordReset(),
            ], false));
        }

        return (new MailMessage)
            ->subject(Lang::get('Reset Password Notification'))
            ->line(Lang::get('You are receiving this email because we received a password reset request for your account.'))
            ->action(Lang::get('Reset Password'), $url)
            ->line(Lang::get('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')]))
            ->line(Lang::get('If you did not request a password reset, no further action is required.'));
    }

    /**
     * Set a callback that should be used when creating the reset password button URL.
     *
     * @param  \Closure  $callback
     * @return void
     */
    public static function createUrlUsing($callback)
    {
        static::$createUrlCallback = $callback;
    }

    /**
     * Set a callback that should be used when building the notification mail message.
     *
     * @param  \Closure  $callback
     * @return void
     */
    public static function toMailUsing($callback)
    {
        static::$toMailCallback = $callback;
    }
}

В методе toMail() видим, откуда взялся маршрут password.reset. Нам нужно вызвать метод createUrlUsing() и определить функцию, которая сформирует правильную ссылку с использованием маршрута user.password.reset. Переопределяем в модели User метод sendPasswordResetNotification().

/* .......... */
use Illuminate\Auth\Notifications\ResetPassword;

class User extends Authenticatable {

    /* .......... */

    public function sendPasswordResetNotification($token) {
        $notification = new ResetPassword($token);
        $notification->createUrlUsing(function ($user, $token) {
            return url(route('user.password.reset', [
                'token' => $token,
                'email' => $user->email
            ]));
        });
        $this->notify($notification);
    }
}

И есть еще одна проблема. Сразу после сброса пароля пользователь аутентифицируется и перенаправляется на страницу /home, которой у нас нет. Исправить это легко — изменяем свойство $redirectTo контроллера App\Http\Controllers\Auth\ResetPasswordController.

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\ResetsPasswords;

class ResetPasswordController extends Controller {

    use ResetsPasswords;

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

Поиск: Laravel • MySQL • PHP • Web-разработка • Аутентификация • База данных • Интернет магазин • Каталог товаров • Пользователь • Практика • Фреймворк • Шаблон сайта

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.