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

05.12.2020

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

Вообще, готовых пакетов для аутентификации существует великое множество, но мы все сделаем сами, чтобы хорошенько разобраться, как это работает. Нам нужно будет создать контроллеры: RegisterController — для регистрации, LoginController — для аутентификации, ForgotPasswordController — для восстановления пароля.

Layout-шаблон

Создаем шаблон resources/views/layout/site.blade.php:

<!doctype html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>{{ $title ?? env('APP_NAME') }}</title>
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
    <script src="{{ asset('js/app.js') }}"></script>
</head>
<body>
<div class="container">
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
        <!-- Логотип и кнопка «Гамбургер» -->
        <a class="navbar-brand" href="/">Блог</a>
        <button class="navbar-toggler" type="button" data-toggle="collapse"
                data-target="#navbar-blog" aria-controls="navbar-blog"
                aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <!-- Основная часть меню (может содержать ссылки, формы и прочее) -->
        <div class="collapse navbar-collapse" id="navbar-blog">
            <!-- Этот блок расположен слева -->
            <ul class="navbar-nav mr-auto">
                <li class="nav-item">
                    <a class="nav-link" href="#">Блог</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="#">Теги</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="#">Контакты</a>
                </li>
            </ul>
            <!-- Этот блок расположен посередине -->
            <form class="form-inline my-2 my-lg-0">
                <input class="form-control mr-sm-2" type="search"
                       placeholder="Поиск по блогу" aria-label="Search">
                <button class="btn btn-outline-info my-2 my-sm-0"
                        type="submit">Искать</button>
            </form>
            <!-- Этот блок расположен справа -->
            <ul class="navbar-nav ml-auto">
                @guest
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('auth.login') }}">Войти</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('auth.register') }}">Регистрация</a>
                    </li>
                @else
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('user.index') }}">Личный кабинет</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('auth.logout') }}">Выйти</a>
                    </li>
                @endif
            </ul>
        </div>
    </nav>

    <div class="row">
        <div class="col-md-3">
            <h4>Категории блога</h4>
            <p>Здесь будут категории блога</p>
            <h4>Популярные теги</h4>
            <p>Здесь будут популярные теги</p>
        </div>
        <div class="col-md-9">
            @if ($message = Session::get('success'))
                <div class="alert alert-success alert-dismissible mt-0" role="alert">
                    <button type="button" class="close" data-dismiss="alert" aria-label="Закрыть">
                        <span aria-hidden="true">&times;</span>
                    </button>
                    {{ $message }}
                </div>
            @endif

            @if ($errors->any())
                <div class="alert alert-danger alert-dismissible mt-4" role="alert">
                    <button type="button" class="close" data-dismiss="alert" aria-label="Закрыть">
                        <span aria-hidden="true">&times;</span>
                    </button>
                    <ul class="mb-0">
                        @foreach ($errors->all() as $error)
                            <li>{{ $error }}</li>
                        @endforeach
                    </ul>
                </div>
            @endif

            @yield('content')
        </div>
    </div>
</div>
</body>
</html>

Чтобы использовать фреймворк bootstrap, выполяняем в консоли две команды:

> composer require laravel/ui
> npm install && npm run dev

Посредники auth и guest

Сразу после установки Laravel доступны посредники auth (для аутентифицированных пользователй) и guest (для не аутентифицированных пользователей) — они определены в файле app/Http/Kernel.php. Мы будем использовать эти посредники, чтобы защитить маршруты контроллеров.

class Kernel extends HttpKernel {
    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        /* ... */
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        /* ... */
    ];
}

Если не аутентифицированный пользователь попробует перейти по маршруту, защищенному посредником auth, он будет перенаправлен на маршрут login. У нас такого маршрута нет, вместо него у нас будет auth.login, так что вносим изменения в класс посредника 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('auth.login');
        }
    }
}

Если аутентифицированный пользователь попробует перейти по маршруту, защищенному посредником guest, он будет перенаправлен на RouteServiceProvider::HOME. Но у нас такой страницы нет, так что вносим изменения в класс посредника RedirectIfAuthenticated.

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;

class RedirectIfAuthenticated {
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next, $guard = null) {
        if (Auth::guard($guard)->check()) {
            // аутентифицированных пользователей отправляем
            // на главную страницу личного кабинета
            return redirect()->route('user.index');
        }
        return $next($request);
    }
}

Регистрация

1. Добавляем маршруты

Добавляем маршруты в файл routes/web.php:

/*
 * Регистрация, вход в ЛК, восстановление пароля
 */
Route::group([
    'as' => 'auth.', // имя маршрута, например auth.index
    'prefix' => 'auth', // префикс маршрута, например auth/index
], function () {
    // форма регистрации
    Route::get('register', 'Auth\RegisterController@register')
        ->name('register');
    // создание пользователя
    Route::post('register', 'Auth\RegisterController@create')
        ->name('create');
});

2. Контроллер RegisterController

Создаем контроллер RegisterController:

> php artisan make:controller Auth/RegisterController
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\User;
use Hash;

class RegisterController extends Controller {

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

    /**
     * Форма регистрации
     */
    public function register() {
        return view('auth.register');
    }

    /**
     * Добавление пользователя
     */
    public function create(Request $request) {
        $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => 'required|string|min:8|confirmed',
        ]);

        User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);

        return redirect()
            ->route('auth.login')
            ->with('success', 'Вы успешно зарегистрировались');
    }
}

Все маршруты контроллера защищены посредником guest, так что аутентифицированный пользователь не сможет перейти по ним.

3. Форма регистрации

Создаем шаблон resources/views/auth/register.blade.php:

@extends('layout.site', ['title' => 'Регистрация'])

@section('content')
    <h1>Регистрация</h1>
    <form method="post" action="{{ route('auth.register') }}">
        @csrf
        <div class="form-group">
            <input type="text" class="form-control" name="name" placeholder="Имя, Фамилия"
                   required maxlength="255" value="{{ old('name') ?? '' }}">
        </div>
        <div class="form-group">
            <input type="email" class="form-control" name="email" placeholder="Адрес почты"
                   required maxlength="255" value="{{ old('email') ?? '' }}">
        </div>
        <div class="form-group">
            <input type="text" class="form-control" name="password" placeholder="Придумайте пароль"
                   required maxlength="255" value="">
        </div>
        <div class="form-group">
            <input type="text" class="form-control" name="password_confirmation"
                   placeholder="Пароль еще раз" required maxlength="255" value="">
        </div>
        <div class="form-group">
            <button type="submit" class="btn btn-info text-white">Регистрация</button>
        </div>
    </form>
@endsection

Аутентификация

1. Добавляем маршруты

Добавляем маршруты в файл routes/web.php:

/*
 * Регистрация, вход в ЛК, восстановление пароля
 */
Route::group([
    'as' => 'auth.', // имя маршрута, например auth.index
    'prefix' => 'auth', // префикс маршрута, например auth/index
], function () {
    // форма регистрации
    Route::get('register', 'Auth\RegisterController@register')
        ->name('register');
    // создание пользователя
    Route::post('register', 'Auth\RegisterController@create')
        ->name('create');
    // форма входа
    Route::get('login', 'Auth\LoginController@login')
        ->name('login');
    // аутентификация
    Route::post('login', 'Auth\LoginController@authenticate')
        ->name('auth');
    // выход
    Route::get('logout', 'Auth\LoginController@logout')
        ->name('logout');
});

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

2. Контроллер LoginController

Создаем контроллер LoginController:

> php artisan make:controller Auth/LoginController
namespace App\Http\Controllers\Auth;

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

class LoginController extends Controller {

    public function __construct() {
        $this->middleware('guest', ['except' => 'logout']);
    }

    /**
     * Форма входа в личный кабинет
     */
    public function login() {
        return view('auth.login');
    }

    /**
     * Аутентификация пользователя
     */
    public function authenticate(Request $request) {
        $request->validate([
            'email' => 'required|string|email',
            'password' => 'required|string',
        ]);

        $credentials = $request->only('email', 'password');

        if (Auth::attempt($credentials)) {
            return redirect()
                ->route('user.index')
                ->with('success', 'Вы вошли в личный кабинет');
        }

        return redirect()
            ->route('auth.login')
            ->withErrors('Неверный логин или пароль');
    }

    /**
     * Выход из личного кабинета
     */
    public function logout() {
        Auth::logout();
        return redirect()
            ->route('auth.login')
            ->with('success', 'Вы вышли из личного кабинета');
    }
}

Все маршруты контроллера (за исключением метода logout()) защищены посредником guest, так что аутентифицированный пользователь не сможет перейти по ним.

3. Форма входа

Создаем шаблон resources/views/auth/login.blade.php:

@extends('layout.site', ['title' => 'Вход в личный кабинет'])

@section('content')
    <h1>Вход в личный кабинет</h1>
    <form method="post" action="{{ route('auth.auth') }}">
        @csrf
        <div class="form-group">
            <input type="email" class="form-control" name="email" placeholder="Адрес почты"
                   required maxlength="255" value="{{ old('email') ?? '' }}">
        </div>
        <div class="form-group">
            <input type="text" class="form-control" name="password" placeholder="Ваш пароль"
                   required maxlength="255" value="">
        </div>
        <div class="form-group">
            <button type="submit" class="btn btn-info text-white">Войти</button>
        </div>
    </form>
@endsection

4. Личный кабинет

Создаем контроллер IndexController:

> php artisan make:controller User/IndexController --invokable
namespace App\Http\Controllers\User;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class IndexController extends Controller {
    /**
     * Handle the incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function __invoke(Request $request) {
        return view('user.index');
    }
}

Создаем шаблон resources/views/user/index.blade.php

@extends('layout.site', ['title' => 'Личный кабинет'])

@section('content')
    <h1>Личный кабинет</h1>
    <p>Добрый день {{ auth()->user()->name }}!</p>
    <p>Это личный кабинет пользователя сайта.</p>
@endsection

Все маршруты контроллера защищены посредником auth, так что попасть в личный кабинет может только аутентифицированный пользователь.

Восстановление пароля

1. Добавляем маршруты

Добавляем маршруты в файл routes/web.php:

/*
 * Регистрация, вход в ЛК, восстановление пароля
 */
Route::group([
    'as' => 'auth.', // имя маршрута, например auth.index
    'prefix' => 'auth', // префикс маршрута, например auth/index
], function () {
    // форма регистрации
    Route::get('register', 'Auth\RegisterController@register')
        ->name('register');
    // создание пользователя
    Route::post('register', 'Auth\RegisterController@create')
        ->name('create');
    // форма входа
    Route::get('login', 'Auth\LoginController@login')
        ->name('login');
    // аутентификация
    Route::post('login', 'Auth\LoginController@authenticate')
        ->name('auth');
    // выход
    Route::get('logout', 'User\LoginController@logout')
        ->name('logout');
    // форма ввода адреса почты
    Route::get('forgot-password', 'Auth\ForgotPasswordController@form')
        ->name('forgot-form');
    // письмо на почту
    Route::post('forgot-password', 'Auth\ForgotPasswordController@mail')
        ->name('forgot-mail');
});

2. Контроллер ForgotPasswordController

Создаем контроллер ForgotPasswordController:

> php artisan make:controller Auth/ForgotPasswordController
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;

class ForgotPasswordController extends Controller {

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

    /**
     * Форма ввода адреса почты
     */
    public function form() {
        return view('auth.forgot');
    }

    /**
     * Письмо на почту для восстановления
     */
    public function mail(Request $request) {
        $request->validate([
            'email' => 'required|email|exists:users',
        ]);
        $token = Str::random(60);
        DB::table('password_resets')->insert(
            ['email' => $request->email, 'token' => $token, 'created_at' => Carbon::now()]
        );
        // ссылка для сброса пароля
        $link = route('auth.reset-form', ['token' => $token, 'email' => $request->email]);
        Mail::send(
            'email.reset-password',
            ['link' => $link],
            function($message) use ($request) {
                $message->to($request->email);
                $message->subject('Восстановление пароля');
            }
        );
        return back()->with('success', 'Ссылка для восстановления пароля отправлена на почту');
    }
}

Все маршруты контроллера защищены посредником guest, так что аутентифицированный пользователь не сможет перейти по ним.

3. Форма ввода адреса

Создаем шаблон resources/views/auth/forgot.blade.php:

@extends('layout.site', ['title' => 'Восстановление пароля'])

@section('content')
    <h1>Восстановление пароля</h1>
    <form method="post" action="{{ route('auth.forgot-mail') }}">
        @csrf
        <div class="form-group">
            <input type="email" class="form-control" name="email" placeholder="Адрес почты"
                   required maxlength="255" value="{{ old('email') ?? '' }}">
        </div>
        <div class="form-group">
            <button type="submit" class="btn btn-info text-white">Восстановить</button>
        </div>
    </form>
@endsection

4. Шаблон для письма

Создаем файл resources/views/email/reset-password.blade.php:

<h3>Восстановление пароля</h3>

<p>Для восстановления пароля перейдите по этой <a href="{{ $link }}">ссылке</a>.</p>

4. Перевод на русский

При вводе адреса почты не зарегистрированного пользователя получаем такое сообщение об ошибке:

В настройках config/app.php у меня указан русский язык, но файлов перевода нет. Строка validation.exists означает, что должно быть показано сообщение exists из файла resources/lang/ru/validation.php. Установим языковый пакет

> composer require laravel-lang/lang:~7.0

После чего скопируем vendor/laravel-lang/lang/src/ru в директорию resources/lang.

5. Отправка письма

На этапе разработки письма отправлять не будем, вместо этого будем их записывать в log-файл. Для этого редактируем файл конфигурации .env:

MAIL_MAILER=log
MAIL_FROM_ADDRESS=noreply@example.com
MAIL_FROM_NAME="${APP_NAME}"
Эти настройки используются в файле config/mail.php, поэтому при отправке письма мы не используем метод from() для установки адреса почты и имени отправителя письма.

Возьмем из базы данных адрес почты любого пользователя и попробуем восстановить пароль:

Проверяем log-файл — письмо, содержащее ссылку для восстановления пароля, было отправлено:

6. Добавляем маршруты

Добавляем маршруты в файл routes/web.php:

/*
 * Регистрация, вход в ЛК, восстановление пароля
 */
Route::group([
    'as' => 'auth.', // имя маршрута, например auth.index
    'prefix' => 'auth', // префикс маршрута, например auth/index
], function () {
    // форма регистрации
    Route::get('register', 'Auth\RegisterController@register')
        ->name('register');
    // создание пользователя
    Route::post('register', 'Auth\RegisterController@create')
        ->name('create');
    // форма входа
    Route::get('login', 'Auth\LoginController@login')
        ->name('login');
    // аутентификация
    Route::post('login', 'Auth\LoginController@authenticate')
        ->name('auth');
    // выход
    Route::get('logout', 'Auth\LoginController@logout')
        ->name('logout');
    // форма ввода адреса почты
    Route::get('forgot-password', 'Auth\ForgotPasswordController@form')
        ->name('forgot-form');
    // письмо на почту
    Route::post('forgot-password', 'Auth\ForgotPasswordController@mail')
        ->name('forgot-mail');
    // форма восстановления пароля
    Route::get(
        'reset-password/token/{token}/email/{email}',
        'Auth\ResetPasswordController@form'
    )->name('reset-form');
    // восстановление пароля
    Route::post('reset-password', 'Auth\ResetPasswordController@reset')
        ->name('reset-password');
});

7. Контроллер ResetPasswordController

Создаем контроллер ResetPasswordController:

> php artisan make:controller Auth/ResetPasswordController
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Hash;

class ResetPasswordController extends Controller {

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

    /**
     * Форма ввода нового пароля
     */
    public function form($token, $email) {
        return view('auth.reset-password', compact('token', 'email'));
    }

    /**
     * Установка нового пароля
     */
    public function reset(Request $request) {
        $request->validate([
            'email' => 'required|email|exists:users',
            'password' => 'required|string|min:8|confirmed',
        ]);
        // удаляем старые записи из таблицы сброса паролей
        $expire = Carbon::now()->subMinute(60);
        DB::table('password_resets')
            ->where('created_at', '<', $expire)
            ->delete();
        // если ссылка на восстановления была отправлена
        $row = DB::table('password_resets')
            ->where([
                'email' => $request->email,
                'token' => $request->token,
            ])
            ->first();
        // если ссылка уже устарела, то ничего не делаем
        if(!$row) {
            return back()->withErrors('Ссылка восстановления пароля устарела');
        }
        // устанавливаем новый пароль для пользователя
        User::where('email', $request->email)
            ->update(['password' => Hash::make($request->password)]);
        // удаляем пользователя из таблицы сброса паролей
        DB::table('password_resets')->where(['email'=> $request->email])->delete();

        return redirect()
            ->route('auth.login')
            ->with('success', 'Ваш пароль был успешно изменен');
    }
}

Все маршруты контроллера защищены посредником guest, так что аутентифицированный пользователь не сможет перейти по ним.

8. Форма ввода пароля

Создаем файл resources/views/auth/reset-password.blade.php:

@extends('layout.site', ['title' => 'Сбросить пароль'])

@section('content')
    <h1>Сбросить пароль</h1>
    <form method="post" action="{{ route('auth.reset-password') }}">
        @csrf
        <input type="hidden" name="email" value="{{ $email }}">
        <input type="hidden" name="token" value="{{ $token }}">
        <div class="form-group">
            <input type="text" class="form-control" name="password"
                   placeholder="Придумайте пароль" required maxlength="255" value="">
        </div>
        <div class="form-group">
            <input type="text" class="form-control" name="password_confirmation"
                   placeholder="Пароль еще раз" required maxlength="255" value="">
        </div>
        <div class="form-group">
            <button type="submit" class="btn btn-info text-white">Сбросить</button>
        </div>
    </form>
@endsection

Поиск: Laravel • MySQL • 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.