Мы сделаем так, чтобы при логине пользователя, он должен будет ввести код подтверждения, отправленный на его почту. В конце статьи вы найдете ссылку на репозиторий 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 IlluminateNotificationsMessagesMailMessage */ 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' => IlluminateAuthMiddlewareAuthorize::class, // ... more middlewares 'twofactor' => AppHttpMiddlewareTwoFactor::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', 'AuthTwoFactorController@resend')->name('verify.resend');
Route::resource('verify', 'AuthTwoFactorController')->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
Перевод: Demiurge Ash

Источник: laravel.demiart.ru