Мы сделаем так, чтобы при логине пользователя, он должен будет ввести код подтверждения, отправленный на его почту. В конце статьи вы найдете ссылку на репозиторий 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.
