Магазин на Laravel 7, часть 8. Регистрация и аутентификация пользователей на сайте
08.10.2020
Теги: Laravel • MySQL • PHP • Web-разработка • Аутентификация • БазаДанных • ИнтернетМагазин • КаталогТоваров • Пользователь • Практика • Фреймворк • ШаблонСайта
В 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">×</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 7, часть 21. Добавляем профили и используем их при оформлении заказа
- Блог на Laravel 7, часть 3. Checkbox «Запомнить меня» и подтверждение адреса почты
- Магазин на Laravel 7, часть 24. Фильтр товаров категории по цене, новинкам и лидерам продаж
- Магазин на Laravel 7, часть 23. Главная страница сайта, новинки, лидеры продаж и распродажа
- Магазин на Laravel 7, часть 20. Показ отдельной страницы и верхнее меню всех страниц
- Магазин на Laravel 7, часть 18. Панель управления, пользователи и CRUD страниц сайта
- Магазин на Laravel 7, часть 17. Панель управления, работа с заказами, изменение статуса
Поиск: Laravel • MySQL • PHP • Web-разработка • Аутентификация • База данных • Интернет магазин • Каталог товаров • Пользователь • Практика • Фреймворк • Шаблон сайта