Недавно я работал над проектом, где одной из главных трудностей были пользовательские пароли. Пользователей завели в систему администраторы, поэтому для них не были заданы пароли. А если бы пришлось вынуждать их вводить и запоминать пароли, то это серьёзно снизило бы юзабилити проекта.
Поэтому мы решили попробовать беспарольный вход в стиле Medium/Slack. Если вы с таким не сталкивались, то в двух словах это работает так: вводите свой email на странице входа в систему, получаете письмо со ссылкой на вход, щёлкаете по ссылке и входите в систему. Доступ к вашему адресу электронной почты удостоверяет вашу личность не требуя пароля.
Давайте вместе сделаем такой вход.
Новое приложение и make:auth
Первым делом создадим наше laravel-приложение и сгенерируем систему аутентификации:
shlaravel new medium-login cd medium-login php artisan make:auth
Теперь у нас есть ряд новых файлов, связанных с аутентификацией, включая страницы входа и регистрации. Давайте начнём с настройки этих файлов.
Изменение страниц входа и регистрации
Страницы входа в систему и регистрации довольно хороши, но нам надо убрать из них поле пароля.
Откройте страницу входа resources/views/auth/login.blade.php и удалите всю группу формы password (метки, поля ввода и обёртку xml<div>
). Сохраните и закройте.
Откройте страницу регистрации resources/views/auth/register.blade.php и также удалите группы формы password и password-reset. Сохраните и закройте.
Возможно, позже вы захотите добавить некоторые инструкции на обеих страницах, описывающие, как будет работать наша аутентификация, а также добавить ссылки для сброса пароля. Но пока этого будет вполне достаточно.
Изменение маршрутов регистрации
Теперь нам нужно обновить маршрут, на который указывают формы входа и регистрации. Давайте перейдём к AuthController и посмотрим, что у нас есть.
Во-первых, мы заметим метод validator, который возвращает валидатор для поля password. Это валидатор для регистрации учётной записи, так что давайте избавимся от этого пароля.
В конечном итоге функция должна выглядеть так:
// app/http/Controllers/Auth/AuthController.php
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
]);
}
И сделаем то же самое для метода create, который также используется для регистрации:
// app/http/Controllers/Auth/AuthController.php
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
]);
}
Переопределение маршрута login
Как вы могли заметить, здесь нет методов для входа пользователей. Они скрыты в типаже AuthenticatesAndRegistersUsers, который просто выдает вам типажи AuthenticatesUsers и RegistersUsers. Вы можете перейти к типажу AuthenticatesUsers и найти в нём метод login, который и позволяет входить пользователям в систему.
Всё, что там происходит, основано на входе с использованием пароля. Поэтому давайте переопределим этот метод до самого основания.
Целью нашего нового метода будет отправка пользователю письма и вывод подсказки о необходимости проверить почту. Давайте перейдем к AuthController и добавим метод login, чтобы переопределить тот, который есть в AuthenticatesUsers.
// app/http/Controllers/Auth/AuthController.php
public function login(Request $request)
{
// проверяем, что это реальный email,
// отправляем письмо для входа,
// показываем пользователю представление с текстом "проверьте почту".
}
Проверка email
Сначала давайте проверим адрес email пользователя. Это довольно просто:
$this->validate($request, ['email' => 'required|email|exists:users']);
Отправка письма для входа в систему
Затем нам надо послать письмо на указанный адрес и пригласить пользователя войти. Тут потребуется немного больше усилий.
Создание структуры для генерации и проверки почтовых токенов входа
Если вы знакомы со структурой базы данных password_reset, вам повезло. Мы будем создавать что-то очень похожее. Каждый раз, когда кто-то пытается войти в систему, мы должны будем добавить в таблицу запись, включающую адрес электронной почты, только что созданный уникальный токен (его мы отправим в электронном письме как часть URL), и дату создания записи для отслеживания срока действия ссылки.
Впоследствии мы будем использовать эту запись, чтобы генерировать (и проверять) вот такие URL: myapp.com/auth/email-authenticate/09ajfpoib23li4ub123p984h1234. Вход в систему по данной ссылке истечёт через заданный промежуток времени. Нам нужно будет связать данный URL с определённым пользователем, таким образом, мы должны зафиксировать email, token и created_at для каждой записи в этой таблице.
Давайте создадим миграцию для неё:
shphp artisan make:migration create_email_logins_table --create=email_logins
И добавим в неё несколько полей:
Schema::create('email_logins', function (Blueprint $table) {
$table->string('email')->index();
$table->string('token')->index();
$table->timestamps();
});
Если хотите, вы можете использовать столбец id внешнего ключа вместо email. Есть несколько причин, по которым использовать его лучше, чем просто email. Но мне нравится просто email. Решать вам.
Теперь давайте создадим модель.
shphp artisan make:model EmailLogin
Отредактируйте файл (app/EmailLogin.php) так, чтобы нам можно было просто создать экземпляр с нужными свойствами:
class EmailLogin extends Model
{
public $fillable = ['email', 'token'];
}
И затем мы захотим связать каждый экземпляр с пользователем. А поскольку в нашем примере мы отслеживаем пользователя по email, а не по id, мы должны вручную привязать столбец таблицы email:
class EmailLogin extends Model
{
public $fillable = ['email', 'token'];
public function user()
{
return $this->hasOne(\App\User::class, 'email', 'email');
}
}
Создание токена
Теперь можно создать письмо. Мы собираемся отправить конечному пользователю письмо с URL, в котором будет сгенерированный ранее уникальный токен.
Сначала давайте определимся, каким образом мы будем создавать и хранить эти токены. Нам нужно создать экземпляр EmailLogin, с этого и начнём:
public function login()
{
$this->validate($request, ['email' => 'required|email|exists:users']);
$emailLogin = EmailLogin::createForEmail($request->input('email'));
}
Давайте добавим этот метод в EmailLogin:
class EmailLogin extends Model
{
...
public static function createForEmail($email)
{
return self::create([
'email' => $email,
'token' => str_random(20)
]);
}
}
Теперь мы создаём экземпляр класса EmailLogin, в котором генерируем рандомный токен, после чего возвращаем этот экземпляр обратно.
Создание URL для отправки на email
Теперь нам надо использовать этот EmailToken для создания URL, который мы можем отправить нашему пользователю в письме.
public function login()
{
$this->validate($request, ['email' => 'required|email|exists:users']);
$emailLogin = EmailLogin::createForEmail($request->input('email'));
$url = route('auth.email-authenticate', [
'token' => $emailLogin->token
]);
}
Давайте создадим маршрут для этого:
// app/Http/routes.php
Route::get('auth/email-authenticate/{token}', [
'as' => 'auth.email-authenticate',
'uses' => 'Auth\AuthController@authenticateEmail'
]);
... и создадим метод контроллера для выполнения этого маршрута:
class AuthController
{
...
public function authenticateEmail($token)
{
$emailLogin = EmailLogin::validFromToken($token);
Auth::login($emailLogin->user);
return redirect('home');
}
}
... и давайте создадим метод validFromToken:
class EmailLogin
{
...
public static function validFromToken($token)
{
return self::where('token', $token)
->where('created_at', '>', Carbon::parse('-15 minutes'))
->firstOrFail();
}
Теперь у нас есть входящий маршрут с проверенным непросроченным токеном, который вводит пользователя и перенаправляет его к home. Давайте отправим это письмо.
Отправка email
Давайте добавим вызов «отправки почты» в метод нашего контроллера:
public function login()
{
...
Mail::send('auth.emails.email-login', ['url' => $url], function ($m) use ($request) {
$m->from('noreply@myapp.com', 'MyApp');
$m->to($request->input('email'))->subject('MyApp Login');
});
xml<!— resources/views/auth/emails/email-login.blade.php —> Войти в систему по этой ссылке: <a href="{{ $url }}">{{ $url }}</a>
Возврат временного представления
Вы можете создать представление каким угодно образом, но в целом нам просто нужно сообщить пользователю «Эй, мы послали тебе письмо, загляни в свой почтовый ящик». И всё.
return 'Письмо для входа в систему отправлено. Проверьте свою электронную почту.';
Соединение всего воедино
Давайте взглянем на нашу систему. У нас есть новый метод login в нашем AuthController:
public function login(Request $request)
{
$this->validate($request, ['email' => 'required|exists:users']);
$emailLogin = EmailLogin::createForEmail($request->input('email'));
$url = route('auth.email-authenticate', [
'token' => $emailLogin->token
]);
Mail::send('auth.emails.email-login', ['url' => $url], function ($m) use ($request) {
$m->from('noreply@myapp.com', 'MyApp');
$m->to($request->input('email'))->subject('MyApp login');
});
return 'Письмо для входа в систему отправлено. Проверьте свою электронную почту.';
}
Мы создали несколько новых представлений. Мы обновили старые представления — убрали поля паролей. Мы создали новый маршрут в /auth/email-authenticate. И мы создали миграцию EmailLogin и класс для всего этого.
Вот и всё!
Готово! Соедините все эти части, и у вас получится полностью функциональная, беспарольная система входа.
Когда ваши пользователи регистрируются в системе, они должны ввести только свой адрес электронной почты. Когда ваши пользователи заходят в систему, они должны ввести только свой адрес электронной почты. Теперь не будет никаких забытых паролей. Вуаля!
Комментарии (11)
https://habrahabr.ru/post/279173/
Дак это ж от меня перевод статьи) На Хабре выложил 14 марта.
Вот пруф: https://habrahabr.ru/post/279173/
Статью здесь переводили, очевидно, с нуля.
Очевидно, провели рефакторинг статьи.
Жаль, вчера не заскринил ибо предисловие было 1 в 1 со статьи на Хабре...
Вот предисловие первой версии статьи здесь:
Вот предисловие с Хабра:
Читаем внимательней предыдущий коммент:
Вчера != сегодня.
Вот именно, читаем внимательнее. Я привёл первую версию, которая была опубликована на сайте. Совпадения заканчиваются после первых 8 слов.
Где пруф, что это первая версия статьи?
В админке сохраняется история правок. Скрин.
Убедительно. Был не прав.
В любом случае, статья уже давно переведена.
Да, мне стоило сказать об этом переводчику. Учтём на будущее.