Статьи

Давайте убьем пароль! Magic Login Ссылки на помощь!

Эта статья была рецензирована Юнесом Рафи и Верном Анчетой . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!


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

Векторная иллюстрация открытого замка на желтом фоне

Общий процесс в системе входа без пароля выглядит следующим образом:

  • Пользователи посещают страницу входа
  • Они вводят свой адрес электронной почты и подтверждают
  • Ссылка отправляется на их электронную почту
  • После нажатия на ссылку они перенаправляются обратно в приложение и входят в систему.
  • Ссылка отключена

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

В этом уроке мы собираемся внедрить такую ​​систему в приложение Laravel. Полный код можно найти здесь .

Создание приложения

Давайте начнем с создания нового приложения Laravel. Я использую Laravel 5.2 в этом уроке:

composer create-project laravel/laravel passwordless-laravel 5.2.* 

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

Настройка базы данных

Затем мы должны настроить нашу базу данных MySQL перед выполнением любых миграций.

Откройте файл .env в корневом каталоге и передайте имя хоста, имя пользователя и имя базы данных:

 [...] DB_CONNECTION=mysql DB_HOST=localhost DB_DATABASE=passwordless-app DB_USERNAME=username DB_PASSWORD= [...] без [...] DB_CONNECTION=mysql DB_HOST=localhost DB_DATABASE=passwordless-app DB_USERNAME=username DB_PASSWORD= [...] 

Если вы используете нашу коробку Homestead Improved , комбинация базы данных / имени пользователя / пароля — это homestead , homestead , secret .

Строительные леса Auth

Одна замечательная вещь, которую Laravel представил в версии 5.2, — это возможность добавить предварительно сделанный уровень аутентификации с помощью одной команды. Давайте сделаем это:

 php artisan make:auth 

Эта команда создает все, что нам нужно для аутентификации, то есть виды, контроллеры и маршруты.

Миграции

Если мы посмотрим на database/migrations , мы заметим, что созданное приложение Laravel поставляется с миграциями для создания таблицы users таблицы password_resets .

Мы ничего не изменим, так как хотим, чтобы у нашего приложения был нормальный поток аутентификации.

Чтобы создать таблицы, запустите:

 php artisan migrate 

Теперь мы можем обслуживать приложение, и пользователи должны иметь возможность зарегистрироваться и войти, используя ссылки в навигационной панели.

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

Перейдите к resources/views/layouts/app.blade.php . Вот где мы находим частичку навигации. Измените строку со ссылкой для входа (прямо под условной версией, чтобы проверить, вышел ли пользователь из системы) на следующую:

ресурсы / просмотров / макеты / app.blade.php

 [...] @if (Auth::guest()) <li><a href="{{ url('/login/magiclink') }}">Login</a></li> <li><a href="{{ url('/register') }}">Register</a></li> [...] 

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

приложение / Http / Промежуточный / Authenticate.php

 class Authenticate { [...] public function handle($request, Closure $next, $guard = null) { if (Auth::guard($guard)->guest()) { if ($request->ajax() || $request->wantsJson()) { return response('Unauthorized.', 401); } else { return redirect()->guest('login/magiclink'); } } return $next($request); } [...] 

Обратите внимание, что внутри else block мы изменили перенаправление, чтобы оно указывало на login/magiclink вместо обычного login .

Создание Magic Login Controller, просмотр и маршруты

Наш следующий шаг — создать MagicLoginController внутри нашей папки Auth:

 php artisan make:controller Auth\\MagicLoginController 

Затем маршрут для отображения нашей пользовательской страницы входа в систему:

приложение / Http / routes.php

 [...] Route::get('/login/magiclink', 'Auth\MagicLoginController@show'); 

Давайте обновим наш MagicLoginController чтобы включить действие шоу:

приложение / HTTP / Контроллеры / Auth / MagicLoginController.php

 class MagicLoginController extends Controller { [...] public function show() { return view('auth.magic.login'); } [...] } 

Для нового вида входа в систему мы заимствуем обычный вид входа в систему, но удалим поле пароля. Мы также изменим URL-адрес сообщения формы, чтобы он указывал на \login\magiclink .

Давайте создадим волшебную папку внутри папки views/auth для хранения этого нового представления:

 mkdir resources/views/auth/magic touch resources/views/auth/magic/login.blade.php 

Давайте обновим наш недавно созданный вид следующим образом:

ресурсы / виды / авториз / магия / login.blade.php

 @extends('layouts.app') @section('content') <div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <div class="panel panel-default"> <div class="panel-heading">Login</div> <div class="panel-body"> <form class="form-horizontal" role="form" method="POST" action="{{ url('/login/magiclink') }}"> {{ csrf_field() }} <div class="form-group{{ $errors->has('email') ? ' has-error' : '' }}"> <label for="email" class="col-md-4 control-label">E-Mail Address</label> <div class="col-md-6"> <input id="email" type="email" class="form-control" name="email" value="{{ old('email') }}" required autofocus> @if ($errors->has('email')) <span class="help-block"> <strong>{{ $errors->first('email') }}</strong> </span> @endif </div> </div> <div class="form-group"> <div class="col-md-6 col-md-offset-4"> <div class="checkbox"> <label> <input type="checkbox" name="remember"> Remember Me </label> </div> </div> </div> <div class="form-group"> <div class="col-md-8 col-md-offset-4"> <button type="submit" class="btn btn-primary"> Send magic link </button> <a href="{{ url('/login') }}" class="btn btn-link">Login with password instead</a> </div> </div> </form> </div> </div> </div> </div> </div> @endsection 

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

Пример просмотра с формой входа

Генерация токенов и связывание их с пользователями

Наш следующий шаг — создать токены и связать их с пользователями. Это происходит, когда кто-то отправляет свою электронную почту для входа в систему.

Давайте начнем с создания маршрута для обработки действия отправки формы входа в систему:

приложение / Http / routes.php

 [...] Route::post('/login/magiclink', 'Auth\MagicLoginController@sendToken'); 

Затем мы добавляем метод контроллера с именем sendToken внутри MagicLoginController . Этот метод проверит адрес электронной почты, свяжет токен с пользователем, отправит электронное письмо с логином и отобразит сообщение, уведомляющее пользователя о необходимости проверить его электронную почту:

приложение / HTTP / Контроллеры / Auth / MagicLoginController.php

 class MagicLoginController extends Controller { [...] /** * Validate that the email has a valid format and exists in the users table * in the email column */ public function sendToken(Request $request) { $this->validate($request, [ 'email' => 'required|email|max:255|exists:users,email' ]); //will add methods to send off a login email and a flash message later } [...] } 

Теперь, когда у нас есть действующий адрес электронной почты, мы можем отправить пользователю электронное письмо с логином. Но перед отправкой электронного письма мы должны сгенерировать токен для пользователя, пытающегося войти в систему. Я не хочу, чтобы все мои методы были в MagicLoginController и, таким образом, мы создадим модель users-token для обработки некоторых из них. методы.

 php artisan make:model UserToken -m 

Эта команда сделает нас и моделью, и миграцией. Нам нужно немного настроить миграцию и добавить user_id и token . Откройте вновь созданный файл миграции и измените метод up следующим образом:

базы данных / миграция / {метка время} _create_user_tokens_table.php

 [...] public function up() { Schema::create('user_tokens', function (Blueprint $table) { $table->increments('id'); $table->integer('user_id'); $table->string('token'); $table->timestamps(); }); } [...] 

Затем выполните команду migrate Artisan:

 php artisan migrate 

В модели UserToken нам нужно добавить user_id и token как часть массовых назначаемых атрибутов. Мы также должны определить отношения, которые эта модель имеет с моделью User и наоборот:

Приложение / UserToken.php

 [...] class UserToken extends Model { protected $fillable = ['user_id', 'token']; /** * A token belongs to a registered user. * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function user() { return $this->belongsTo(User::class); } } 

Затем внутри App/User.php укажите, что с User может быть связан только один токен:

Приложение / User.php

 class User extends Model { [...] /** * A user has only one token. * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function token() { return $this->hasOne(UserToken::class); } } 

Давайте теперь сгенерируем токен. Во-первых, нам нужно извлечь объект пользователя по его электронной почте перед созданием токена. Создайте метод в модели User именем getUserByEmail для обработки этой функциональности:

Приложение / User.php

 class User extends Model { [...] protected static function getUserByEmail($value) { $user = self::where('email', $value)->first(); return $user; } [...] } 

Мы должны UserToken пространства имен для классов User и UserToken в наш MagicLoginController , чтобы иметь возможность вызывать методы этих классов из нашего контроллера:

приложение / HTTP / Контроллеры / Auth / MagicLoginController.php

 [...] use App\User; use App\UserToken; [...] class MagicLoginController extends Controller { [...] public function sendToken(Request $request) { //after validation [...] $user = User::getUserByEmail($request->get('email')); if (!user) { return redirect('/login/magiclink')->with('error', 'User not foud. PLease sign up'); } UserToken::create([ 'user_id' => $user->id, 'token' => str_random(50) ]); } [...] } 

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

Когда у нас есть пользовательский объект, мы генерируем для него токен.

Отправка токена по электронной почте

Теперь мы можем отправить созданный токен пользователю по электронной почте в виде URL. Во-первых, нам понадобится Mail Facade в нашей модели, чтобы помочь нам с функцией отправки электронной почты.

Однако в этом уроке мы не будем отправлять реальные письма. Просто подтверждая, что приложение может отправить электронное письмо в журналах. Для этого перейдите к вашему файлу .env и в разделе почты установите MAIL_DRIVER=log . Кроме того, мы не будем создавать представления электронной почты; просто отправив необработанное письмо из нашего класса UserToken .

Давайте создадим метод в нашей модели sendEmail под названием sendEmail для обработки этой функциональности. URL, который является комбинацией token , email address и значения remember me будет создан внутри этого метода:

Приложение / UserToken.php

 [...] use Illuminate\Support\Facades\Mail; [...] class UserToken extends Model { [...] public static function sendMail($request) { //grab user by the submitted email $user = User::getUserByEmail($request->get('email')); if(!$user) { return redirect('/login/magiclink')->with('error', 'User not foud. PLease sign up'); } $url = url('/login/magiclink/' . $user->token->token . '?' . http_build_query([ 'remember' => $request->get('remember'), 'email' => $request->get('email'), ])); Mail::raw( "<a href='{$url}'>{$url}</a>", function ($message) use ($user) { $message->to($user->email) ->subject('Click the magic link to login'); } ); } [...] } 

При создании URL мы будем использовать PHP-функцию http_build_query чтобы помочь нам сделать запрос из массива переданных опций. В нашем случае это электронная почта и запомни меня. После создания URL мы отправляем его пользователю по почте.

Пора обновить наш MagicLoginController и вызвать метод sendEmail :

приложение / HTTP / Контроллеры / Auth / MagicLoginController.php

 class MagicLoginController extends Controller { [...] public function sendToken(Request $request) { $this->validate($request, [ 'email' => 'required|email|max:255|exists:users,email' ]); UserToken::storeToken($request); UserToken::sendMail($request); return back()->with('success', 'We\'ve sent you a magic link! The link expires in 5 minutes'); } [...] } 

Мы также собираемся реализовать некоторые базовые флэш-сообщения для уведомлений. В ваших resources/views/layouts/app.blade.php вставьте этот блок прямо над вашим content поскольку флеш-сообщения отображаются вверху перед любым другим контентом:

ресурсы / просмотров / макеты / app.blade.php

 [...] <div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> @include ('layouts.partials._notifications') </div> </div> </div> @yield('content') [...] 

Затем создайте частичное уведомление:

ресурсы / виды / макеты / обертоны / _notifications.blade.php

 @if (session('success')) <div class="alert alert-success"> {{ session('success') }} </div> @endif @if (session('error')) <div class="alert alert-danger"> {{ session('error') }} </div> @endif 

В частности, мы использовали помощника session чтобы помочь нам с различными цветами уведомлений в зависимости от состояния сеанса, то есть success или error .

На данный момент мы можем отправлять электронные письма. Мы можем попробовать это, войдя в систему с действительным адресом электронной почты, а затем laravel.log файлу laravel.log . Мы должны увидеть письмо, содержащее URL внизу журналов.

Затем мы хотим проверить токен и войти в систему пользователя. Мы не хотим, чтобы токен, который был отправлен 3 дня назад, все еще может использоваться для входа в систему.

Проверка токена и аутентификация

Теперь, когда у нас есть URL, давайте создадим маршрут и действие контроллера для обработки того, что происходит, когда кто-то нажимает на URL из их электронного письма:

приложение / Http / routes.php

 [...] Route::get('/login/magiclink/{token}', 'Auth\MagicLoginController@authenticate'); 

Давайте создадим действие authenticate в MagicLoginController . Именно внутри этого метода мы будем аутентифицировать пользователя. Мы собираемся вставить токен в метод authenticate через привязку модели маршрута . Затем мы заберем пользователя из токена. Обратите внимание, что мы должны Auth facade в контроллер, чтобы можно было использовать методы Auth :

приложение / HTTP / Контроллеры / Auth / MagicLoginController.php

 [...] use Auth; [...] class MagicLoginController extends Controller { [...] public function authenticate(Request $request, UserToken $token) { Auth::login($token->user, $request->remember); $token->delete(); return redirect('home'); } [...] } 

Затем в UserToken class задайте имя ключа маршрута, которое мы ожидаем. В нашем случае это токен:

Приложение / UserToken.php

 [...] public function getRouteKeyName() { return 'token'; } [...] 

И там у нас это есть. Теперь пользователи могут войти в систему. Обратите внимание, что после входа в систему мы удаляем токен, поскольку не хотим заполнять таблицу user_tokens использованными токенами.

Наш следующий шаг — проверка, если токен все еще действителен. В этом приложении срок действия магической ссылки истекает через 5 минут. Нам потребуется библиотека Carbon, чтобы помочь нам проверить разницу во времени между временем создания токена и текущим временем.

В нашей модели UserToken мы собираемся создать два метода: isExpired и belongsToEmail чтобы проверить действительность токена. Обратите внимание, что проверка belongsToEmail — это просто дополнительная мера предосторожности, позволяющая убедиться, что токен действительно принадлежит этому адресу электронной почты:

Приложение / UserToken.php

 [...] use Carbon\Carbon; [...] class UserToken extends Model { [...] //Make sure that 5 minutes have not elapsed since the token was created public function isExpired() { return $this->created_at->diffInMinutes(Carbon::now()) > 5; } //Make sure the token indeed belongs to the user with that email address public function belongsToUser($email) { $user = User::getUserByEmail($email); if(!$user || $user->token == null) { //if no record was found or record found does not have a token return false; } //if record found has a token that matches what was sent in the email return ($this->token === $user->token->token); } [...] } 

Давайте вызовем методы для экземпляра токена в методе authenticate в MagicLoginController :

приложение / HTTP / Контроллеры / Auth / MagicLoginController.php

 class MagicLoginController extends Controller { [...] public function authenticate(Request $request, UserToken $token) { if ($token->isExpired()) { $token->delete(); return redirect('/login/magiclink')->with('error', 'That magic link has expired.'); } if (!$token->belongsToUser($request->email)) { $token->delete(); return redirect('/login/magiclink')->with('error', 'Invalid magic link.'); } Auth::login($token->user, $request->get('remember')); $token->delete(); return redirect('home'); } [...] } 

Вывод

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

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

Не думаете ли вы, что пришло время предоставить пользователям альтернативный способ входа в ваш следующий проект?

Пожалуйста, оставьте свои комментарии и вопросы ниже, и не забудьте поделиться этим постом с друзьями и коллегами, если вам понравилось!