Эта статья была рецензирована Юнесом Рафи и Верном Анчетой . Спасибо всем рецензентам 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'); } [...] }
Вывод
Мы успешно добавили логин без пароля поверх обычного потока аутентификации. Некоторые могут поспорить, что это займет больше времени, чем при обычном входе в систему с паролем, но и при использовании менеджера паролей.
Системы без пароля не будут работать везде, хотя, если у вас короткие периоды ожидания сеанса или вы ожидаете, что пользователи будут часто входить в систему, это может расстроить вас. К счастью, это затрагивает очень мало сайтов.
Не думаете ли вы, что пришло время предоставить пользователям альтернативный способ входа в ваш следующий проект?
Пожалуйста, оставьте свои комментарии и вопросы ниже, и не забудьте поделиться этим постом с друзьями и коллегами, если вам понравилось!