[Перевод] Создание беспарольной аутентификации в Laravel, используя только email

Недавно я работал над проектом, где одной из болевых точек был пароль пользователя. Администратор добавлял пользователей в приложение, поэтому они не имеют пароля, а заставлять их придумывать пароль при первом после регистрации входе было крайне неудобно.

Итак, мы решили попробовать метод беспарольного входа. Если Вы никогда не имели возможности работать с этим, мы расскажем как это работает:

На странице входа в систему пользователь вводит свой email-адрес, на который получит ссылку на вход. Переход по ссылке подтверждает личность пользователя без необходимости ввода пароля, так как ссылка для каждого входа пользователя уникальная.

Начнем творить!

image

Новое приложение и make: auth


Вначале мы создадим наше приложение, подключив аутентификацию:

laravel new medium-login
cd medium-login
php artisan make:auth

Теперь у нас есть все необходимые для авторизации файлы, в том числе вьюхи. Давайте начнем с них.

Изменение страницы входа и регистрации


Конечно, сочетание логина с паролем довольно хорошая идея, но нам нужно отказаться от поля ввода пароля на обеих формах.

Откройте файл `resources/views/auth/login.blade.php` и удалите группу, отвечающую за ввод пароля (label, input и обертка). Сохраняем, закрываем.

Теперь открываем файл `resources/views/auth/register.blade.php` и удаляем группы, отвечающие за ввод пароля (`password`) и подтверждения пароля (`password-reset`). Сохраняем, закрываем.

Позже Вы можете добавить инструкцию о методе входа на странице аутентификации, а также разместите ссылки на сброс пароля, но это позже.

Изменение регистрационных роутов


Итак, нам нужно изменить роут, указывающий на точки входа и регистрации. Взглянем на контроллер ` AuthController`.

Во-первых, мы заметим метод `validator`, возвращающий валидацию поля пароля. Так как он отвечает за процесс регистрации учетной записи, нам нужно избавиться от его привязки к паролю.

В конечном итоге, функция должна выглядеть так:

// 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`.

Все, что там происходит, основывается на защищенных паролях, хотя этот метод можно и заменить…

Целью нашего нового метода является отправка на email пользователя ссылки для входа в систему. Давайте вернемся к контроллеру `AuthController` и добавим метод входа в систему, перекрывающий `login` в `AuthenticatesUsers`:

// app/http/Controllers/Auth/AuthController.php
public function login(Request $request)
{
    // validate that this is a real email address
    // send off a login email
    // show the users a view saying "check your email"
}

Подтверждение реальности email-адреса


Подтвердить реальность email адреса для зарегистрированного пользователя очень просто:

$this->validate($request, ['email' => 'required|email|exists:users']);

Отправка email-сообщения


Далее, нам необходимо отправить пользователю ссылку на вход. Это займет немного больше времени.

Создание структуры для формирования и проверки токенов email


Если Вы знакомы с формой структуры базы данных `password_reset`, то Вам будет проще, т.к. мы будем создавать нечто похожее. Каждый раз, когда кто-то пытается войти в систему, нам нужно добавлять запись в таблицу, которая будет фиксировать адрес электронной почты и уникальный токен, отправляемые в электронном письме в качестве URL, а также дату создания и срок жизни записи.

В конечном итоге мы будем использовать URL-адрес для создания (и проверки), например: `myapp.com/auth/email-authenticate/09ajfpoib23li4ub123p984h1234`. Так как срок жизни токена ограничен, мы должны связать этот URL с конкретным пользователем, отслеживая email, токен и дату создания для каждой записи таблицы.

Итак, создадим для него миграцию:

php 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` вместо токена, но есть несколько причин более лучших вариантов. Во всяком случае, решать Вам.

Теперь, давайте создадим модель.

php artisan make:model EmailLogin

Отредактировать файл (`app/EmailLogin.php`) и сделать его простым для нас, создав экземпляр с нужными свойствами:

class EmailLogin extends Model
{
    public $fillable = ['email', 'token'];
}

И когда хотим найти пользователя, мы должны использовать электронную почту, а не идентификатор, вручную связав столбец email:

class EmailLogin extends Model
{
    public $fillable = ['email', 'token'];

    public function user()
    {
        return $this->hasOne(\App\User::class, 'email', '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)
        ]);
    }
}

Мы генерируем рандомный токен и создаем экземпляр класса `EmailToken`, получая его обратно.

Формирование 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();
    }

Теперь у нас есть входящий роут, учитывающий актуальность каждого токена. Если токен актуален — пользователь будет перенаправлен по адресу `mysite.ru/home`.

Что ж, давайте отправим письмо.

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


Добавим вызов `call 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');
    });

… и создадим шаблон:


Log in to MyApp here: {{ $url }}

Возвращение шаблона


Вы можете оформить шаблон любым удобным способом, но мы просто используем текст: «Эй, мы отправили мыло, чтобы его проверить. Это все.»

return 'Login email sent. Go check your email.';

Совместный вход


Взглянем на нашу систему. У нас есть новый метод `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 'Login email sent. Go check your email.';
}

Мы создали несколько вьюх, обновив существующие (убрали в них записи о пароле). Также создали новый роут в `/auth/email-authenticate`. И также создали миграцию `EmailLogin` с классом всех его потребностей.

Это все!


И… профит! Поместите все примеры в Ваш код и получите полностью функциональную систему беспарольного входа.

Для регистрации пользователя нужно будет узнать всего-лишь их email-адрес. И при авторизации кроме их email-адреса больше ничего не нужно будет запоминать и вводить. Больше нет забытых паролей. Бум!

От переводчика


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

© Habrahabr.ru