Мы сделаем так, чтобы при логине пользователя, он должен будет ввести код подтверждения, отправленный на его почту. В конце статьи вы найдете ссылку на репозиторий Github с полным кодом проекта.
В основе проекта будет админка на Laravel 6, созданная с помощью QuickAdminPanel, но вы можете начать с «пустого» дефолтого Laravel Auth, шаги будут работать точно так же.
Шаг 1. Два новых поля в таблице Users
Наша новая миграция:
Schema::table('users', function (Blueprint $table) { $table->string('two_factor_code')->nullable(); $table->dateTime('two_factor_expires_at')->nullable(); });
Поле two_factor_code
будет содержать случайное 6-значное число, а two_factor_expires_at
будет содержать его время жизни — в нашем случае, срок истекает через 10 минут.
Также добавляем эти поля в app/User.php
— в массивы $fillable
и X:
class User extends Authenticatable { protected $dates = [ 'updated_at', 'created_at', 'deleted_at', 'email_verified_at', 'two_factor_expires_at', ]; protected $fillable = [ 'name', 'email', 'password', 'created_at', 'updated_at', 'deleted_at', 'remember_token', 'email_verified_at', 'two_factor_code', 'two_factor_expires_at', ];
Шаг 2. Генерация от отправка кода при логине
Метод, который нужно добавить в app/Http/Controllers/Auth/LoginController.php
:
protected function authenticated(Request $request, $user) { $user->generateTwoFactorCode(); $user->notify(new TwoFactorCode()); }
Таким образом, мы переопределяем метод authenticated()
ядра Laravel и добавляем логику того, что должно происходить после входа пользователя в систему.
Сначала мы генерируем код и добавляем для этого метод в app/User.php
:
public function generateTwoFactorCode() { $this->timestamps = false; $this->two_factor_code = rand(100000, 999999); $this->two_factor_expires_at = now()->addMinutes(10); $this->save(); }
Помимо установки двухфакторного кода и срока его действия, мы также указываем, что это обновление не должно касаться столбца updated_at
в таблице users
, соответственно мы делаем $this->timestamps = false;
.
Теперь, обратно в LoginController. Мы вызываем $user->notify()
и используем систему уведомлений Laravel. Для этого нам нужно создать класс уведомлений с помощью
php artisan make: messages TwoFactorCode
И пишем в app/Notifications/TwoFactorCode.php
:
class TwoFactorCode extends Notification { /** * Get the mail representation of the notification. * * @param mixed $notifiable * @return \Illuminate\Notifications\Messages\MailMessage */ public function toMail($notifiable) { return (new MailMessage) ->line('Ваш двухфакторый код: '.$notifiable->two_factor_code) ->action('Проверить', route('verify.index')) ->line('Срок действия кода — 10 минут') ->line('Если вы не пытались войти на сайт, то проигнорируйте это сообщение.'); } }
Этот код отправляет такое письмо:
Здесь нужно упомянуть две вещи:
- Параметр
$notifiable
методаtoMail()
автоматически назначается как объект залогиненного пользователя, поэтому мы можем получить доступ к столбцуusers.two_factor_code
через$notifiable->two_factor_code;
- Чуть позже мы создадим маршрут
route ('verify.index')
, который повторно отправляет код.
Шаг 3. Показываем форму подтверждения с Мидлваром
После того, как пользователь войдет в систему и получит письмо с кодом подтверждения, он будет перенаправлен на следующую форму:
Фактически, они увидят эту форму, если они введут любой URL. Она будет им показываться до тех пор, пока они не введут проверочный код.
Для этого мы сгенерируем мидлвар:
php artisan make: middleware TwoFactor
И заполним его app/Http/Middleware/TwoFactor.php
:
class TwoFactor { public function handle($request, Closure $next) { $user = auth()->user(); if(auth()->check() && $user->two_factor_code) { if($user->two_factor_expires_at->lt(now())) { $user->resetTwoFactorCode(); auth()->logout(); return redirect()->route('login') ->withMessage('Срок действия двухфакторного кода истек. Пожалуйста, войдите еще раз.'); } if(!$request->is('verify*')) { return redirect()->route('verify.index'); } } return $next($request); } }
Если вы не знаете, как работает мидлвар (middleware), прочитайте официальную документацию Laravel. В основном это класс, который выполняет некоторые действия, обычно ограничивающие доступ к какой-либо странице или функции.
Итак, в нашем случае мы проверяем, существует ли двухфакторный кодовый набор. Если да, то проверяем, не истек ли он. Если срок его действия истек, то сбрасываем его и перенаправляем обратно на форму входа. Если он все еще активен, то перенаправляем обратно на форму подтверждения.
Другими словами, если users.two_factor_code
пустой, то значит он проверен и пользователь может двигаться дальше.
Вот код метода resetTwoFactorCode()
в app/User.php
:
public function resetTwoFactorCode() { $this->timestamps = false; $this->two_factor_code = null; $this->two_factor_expires_at = null; $this->save(); }
Далее, мы назначаем нашему мидлвару «псевдоним» в app/Http/Kernel.php
:
class Kernel extends HttpKernel { // ... protected $routeMiddleware = [ 'can' => \Illuminate\Auth\Middleware\Authorize::class, // ... more middlewares 'twofactor' => \App\Http\Middleware\TwoFactor::class, ]; }
Теперь нам нужно назначить это twofactor
мидлвар некоторым маршрутам. В нашем случае это группа маршрутов в routes/web.php
, сгенерированная QuickAdminPanel:
Route::group([ 'prefix' => 'admin', 'as' => 'admin.', 'namespace' => 'Admin', 'middleware' => ['auth', 'twofactor'] ], function () { Route::resource('permissions', 'PermissionsController'); Route::resource('roles', 'RolesController'); Route::resource('users', 'UsersController'); });
Шаг 4. Контроллер/Шаблон страницы проверки
На этом этапе любой запрос на любой URL будет перенаправлен на проверку кода. Для этого у нас будет два дополнительных маршрута:
Route::get('verify/resend', 'Auth\TwoFactorController@resend')->name('verify.resend'); Route::resource('verify', 'Auth\TwoFactorController')->only(['index', 'store']);
Основная логика будет в app/Http/Controllers/Auth/TwoFactorController.php
:
class TwoFactorController extends Controller { public function index() { return view('auth.twoFactor'); } public function store(Request $request) { $request->validate([ 'two_factor_code' => 'integer|required', ]); $user = auth()->user(); if($request->input('two_factor_code') == $user->two_factor_code) { $user->resetTwoFactorCode(); return redirect()->route('admin.home'); } return redirect()->back() ->withErrors(['two_factor_code' => 'The two factor code you have entered does not match']); } public function resend() { $user = auth()->user(); $user->generateTwoFactorCode(); $user->notify(new TwoFactorCode()); return redirect()->back()->withMessage('Двухфакторный код отправлен снова'); } }
Основная форма находится в методе index()
, затем она передает данные в метод store()
для проверки кода, а метод resend()
предназначен для повторной генерации и отправки кода новым письмом.
Давайте посмотрим на форму подтверждения — в resources/views/auth/twoFactor.blade.php
:
@if(session()->has('message')) <p class="alert alert-info"> {{ session()->get('message') }} </p> @endif <form method="POST" action="{{ route('verify.store') }}"> {{ csrf_field() }} <h1>Two Factor Verification</h1> <p class="text-muted"> You have received an email which contains two factor login code. If you haven't received it, press <a href="{{ route('verify.resend') }}">here</a>. </p> <div class="input-group mb-3"> <div class="input-group-prepend"> <span class="input-group-text"> <i class="fa fa-lock"></i> </span> </div> <input name="two_factor_code" type="text" class="form-control{{ $errors->has('two_factor_code') ? ' is-invalid' : '' }}" required autofocus placeholder="Two Factor Code"> @if($errors->has('two_factor_code')) <div class="invalid-feedback"> {{ $errors->first('two_factor_code') }} </div> @endif </div> <div class="row"> <div class="col-6"> <button type="submit" class="btn btn-primary px-4"> Verify </button> </div> </div> </form>
Я намеренно пропустил весь «родительский» HTML шаблон, потому что он зависит от вашего дизайна, а это основная форм в Blade. Думаю, что тут всё само собой понятно.
Единственное, что может нуждается в объяснении, это session()->has(‘message’)
— откуда это сообщение? Оно из контроллера из метода resend()
. Вот его последняя строка:
return redirect()->back()->withMessage('Двухфакторный код отправлен снова');
Вдруг вы не знаете, но метод redirect()
в Laravel можно использовать цепочечным методом (chained method) ->withWhwhat('text')
. Этот текст сохраняется в сессии как session(‘whatever’)
(в нижнем регистре).
Иии, вот и все! У нас есть полная логика для отправки двухфакторного кода по электронной почте.
Единственное, что я не затронул, так как это индивидуально — КОГДА вы захотите отправить этот код. В нашем примере мы отправляем его каждый раз, но это не для реальной жизни. Вам нужно будет отправлять код всякий раз, когда, например, логинятся с нового IP-адреса, или из другой страны, или каждый 5-й раз, или по каким-либо другим условиям. Поэтому, пожалуйста, сделайте его сами — просто отредактируйте Middleware и LoginController, чтобы сказать Laravel когда нужно его отправить.
Ссылка на полный репозиторий: https://github.com/LaravelDaily/Laravel-Two-Factor-Auth-Email
Автор: Povilas Korop
Перевод: Алексей Широков
Наш Телеграм-канал — следите за новостями о Laravel.