Эта статья была рецензирована Кристофером Томасом . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!
В то время как все обеспокоены безопасностью своего приложения, лишь немногие относятся к этому серьезно и делают решающий шаг Первое, что вы заметите, узнав об этом, — это то, что двухфакторная аутентификация (2FA) является первым решением.
Хотя были некоторые серьезные проблемы с использованием текстовых сообщений в качестве второго фактора , это определенно более безопасно, чем простая комбинация имени пользователя и пароля, учитывая, что многие пользователи склонны использовать популярные и легко угадываемые пароли для таких важных услуг, как платежи, чат, электронная почта и т. д. В этой статье мы собираемся встроить двухфакторную аутентификацию в приложение Laravel, используя в качестве второго фактора Twilio SMS .
Что мы строим
Есть большой шанс, что вы уже знакомы с потоком 2FA:
-
Пользователь заходит на страницу входа.
-
Он вводит электронную почту и пароль.
-
Мы отправляем проверочный код по номеру телефона.
-
Пользователь должен ввести полученный код.
-
Если код верен, мы регистрируем их. В противном случае мы даем им еще один шанс попробовать войти.
Окончательное демонстрационное приложение на GitHub .
Установка
Я предполагаю, что ваша среда разработки уже настроена. Если нет, мы рекомендуем Homestead Improved для быстрого старта.
Создайте новый проект Laravel, используя установщик Laravel или Composer.
laravel new demo
Или
composer create-project --prefer-dist laravel/laravel demo
Отредактируйте файл .env
и добавьте свои учетные данные базы данных.
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=homestead DB_USERNAME=root DB_PASSWORD=root
Аутентификация лесов
Перед созданием наших миграций, имейте в виду, что у Laravel есть команда, чтобы помочь нам защитить наш процесс аутентификации. Он генерирует следующее:
- Вход, регистрация и сброс пароля просмотра и контроллеров.
- Необходимые маршруты.
Запустите php artisan make:auth
из командной строки.
Создание миграций
Обновите класс миграции users
и добавьте поля country_code
и phone
.
// database/migrations/2014_10_12_000000_create_users_table.php class CreateUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->string('email')->unique(); $table->string('password'); $table->string('country_code', 4)->nullable(); $table->string('phone')->nullable(); $table->rememberToken(); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('users'); } }
У каждого пользователя есть список сгенерированных им токенов (проверочных кодов). Запустите команду php artisan make:model Token -m
чтобы создать модель и файл миграции. Схема таблицы будет выглядеть так:
// database/migrations/2016_12_14_105000_create_tokens_table.php class CreateTokensTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('tokens', function (Blueprint $table) { $table->increments('id'); $table->string('code', 4); $table->integer('user_id')->unsigned(); $table->boolean('used')->default(false); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('tokens'); } }
по// database/migrations/2016_12_14_105000_create_tokens_table.php class CreateTokensTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('tokens', function (Blueprint $table) { $table->increments('id'); $table->string('code', 4); $table->integer('user_id')->unsigned(); $table->boolean('used')->default(false); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('tokens'); } }
Я ограничил проверочный код четырьмя цифрами, но вы можете усложнить его, увеличив его. Мы вернемся к этому вопросу позже. Давайте запустим php artisan migrate
для создания нашей базы данных.
Обновление моделей
Модели уже есть и должны обновляться только соответственно:
// app/User.php class User extends Authenticatable { use Notifiable; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'email', 'password', 'country_code', 'phone' ]; /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ 'password', 'remember_token', ]; /** * User tokens relation * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function tokens() { return $this->hasMany(Token::class); } /** * Return the country code and phone number concatenated * * @return string */ public function getPhoneNumber() { return $this->country_code.$this->phone; } }
Здесь нет ничего особенного, мы просто добавили отношение users -> tokens
и добавили вспомогательный метод getPhoneNumber
чтобы получить полный номер телефона пользователя.
// app/Token.php class Token extends Model { const EXPIRATION_TIME = 15; // minutes protected $fillable = [ 'code', 'user_id', 'used' ]; public function __construct(array $attributes = []) { if (! isset($attributes['code'])) { $attributes['code'] = $this->generateCode(); } parent::__construct($attributes); } /** * Generate a six digits code * * @param int $codeLength * @return string */ public function generateCode($codeLength = 4) { $min = pow(10, $codeLength); $max = $min * 10 - 1; $code = mt_rand($min, $max); return $code; } /** * User tokens relation * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function user() { return $this->belongsTo(User::class); } /** * True if the token is not used nor expired * * @return bool */ public function isValid() { return ! $this->isUsed() && ! $this->isExpired(); } /** * Is the current token used * * @return bool */ public function isUsed() { return $this->used; } /** * Is the current token expired * * @return bool */ public function isExpired() { return $this->created_at->diffInMinutes(Carbon::now()) > static::EXPIRATION_TIME; } }
Помимо настройки методов отношений и обновления атрибутов fillable, мы добавили:
- конструктор для установки свойства кода при создании.
- метод
generateCode
для генерации случайных цифр в зависимости от параметра длины кода. - метод
isExpired
чтобы увидеть, истек ли срок действия ссылки с помощью константыEXPIRATION_TIME
. - метод
isValid
чтобы увидеть, не истекла ли ссылка и не используется ли она.
Создание видов
Файл представления register
должен быть обновлен, чтобы включить код страны и поле телефона.
// resources/views/auth/register.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">Register</div> <div class="panel-body"> @include("partials.errors") <form class="form-horizontal" role="form" method="POST" action="{{ url('/register') }}"> // ... <div class="form-group"> <label for="phone" class="col-md-4 control-label">Phone</label> <div class="col-md-6"> <div class="input-group"> <div class="input-group-addon"> <select name="country_code" style="width: 150px;"> <option value="+1">(+1) US</option> <option value="+212">(+212) Morocco</option> </select> </div> <input id="phone" type="text" class="form-control" name="phone" required> @if ($errors->has('country_code')) <span class="help-block"> <strong>{{ $errors->first('country_code') }}</strong> </span> @endif @if ($errors->has('phone')) <span class="help-block"> <strong>{{ $errors->first('phone') }}</strong> </span> @endif </div> </div> </div> // ... </form> </div> </div> </div> </div> </div> @endsection
Затем мы создаем новое представление для пользователя, чтобы ввести код подтверждения.
// resources/views/auth/code.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"> @include("partials.errors") <form class="form-horizontal" role="form" method="POST" action="{{ url('/code') }}"> {{ csrf_field() }} <div class="form-group"> <label for="code" class="col-md-4 control-label">Four digits code</label> <div class="col-md-6"> <input id="code" type="text" class="form-control" name="code" value="{{ old('code') }}" required autofocus> @if ($errors->has('code')) <span class="help-block"> <strong>{{ $errors->first('code') }}</strong> </span> @endif </div> </div> <div class="form-group"> <div class="col-md-8 col-md-offset-4"> <button type="submit" class="btn btn-primary"> Login </button> </div> </div> </form> </div> </div> </div> </div> </div> @endsection
errors.blade.php
напечатает список ошибок проверки.
// resources/views/errors.blade.php @if (count($errors) > 0) <div class="alert alert-danger "> <button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span></button> <ul> @foreach ($errors->all() as $error) <li>{{ $error }}</li> @endforeach </ul> </div> @endif
Создание контроллеров
Вместо создания новых контроллеров, почему бы не использовать контроллеры аутентификации? В конце концов, добавить особо нечего!
Метод RegisterController@register
вызывается, когда пользователь публикует форму, и если вы откроете файл, вы обнаружите, что он вызывает registered
метод после создания пользователя.
// app/Http/Controllers/RegisterController.php class RegisterController extends Controller { // ... protected function registered(Request $request, $user) { $user->country_code = $request->country_code; $user->phone = $request->phone; $user->save(); } }
Нам также необходимо обновить проверку запроса и сделать обязательными поля кода страны и телефона.
// app/Http/Controllers/RegisterController.php class RegisterController extends Controller { // ... protected function validator(array $data) { return Validator::make($data, [ 'name' => 'required|max:255', 'email' => 'required|email|max:255|unique:users', 'password' => 'required|min:6|confirmed', 'country_code' => 'required', 'phone' => 'required' ]); } }
Теперь нам нужно обновить LoginController
и переопределить метод login
.
// app/Http/Controllers/LoginController.php class LoginController extends Controller { // ... public function login(Request $request) { $this->validateLogin($request); //retrieveByCredentials if ($user = app('auth')->getProvider()->retrieveByCredentials($request->only('email', 'password'))) { $token = Token::create([ 'user_id' => $user->id ]); if ($token->sendCode()) { session()->set("token_id", $token->id); session()->set("user_id", $user->id); session()->set("remember", $request->get('remember')); return redirect("code"); } $token->delete();// delete token because it can't be sent return redirect('/login')->withErrors([ "Unable to send verification code" ]); } return redirect()->back() ->withInputs() ->withErrors([ $this->username() => Lang::get('auth.failed') ]); } }
После проверки запроса мы пытаемся получить пользователя, используя адрес электронной почты и пароль. Если пользователь существует, мы создаем новый токен для этого пользователя, затем отправляем код, устанавливаем необходимые сведения о сеансе и перенаправляем на кодовую страницу.
Ой, подожди! Мы не определили метод sendCode
внутри модели Token
?
Добавление Twilio
Перед отправкой кода пользователю через SMS нам необходимо настроить Twilio для работы. Нам нужно создать новый пробный аккаунт .
После этого перейдите на страницу консоли Twilio и скопируйте идентификатор своей учетной записи и токен авторизации . Последняя часть — создать новый номер телефона для отправки SMS. Перейдите на страницу телефонных номеров консоли и создайте новую.
TWILIO_SID=XXXXXXXXXXX TWILIO_TOKEN=XXXXXXXXXXXX TWILIO_NUMBER=+XXXXXXXXXX
Twilio имеет официальный пакет PHP, который мы можем использовать.
composer require twilio/sdk
Чтобы использовать пакет Twilio, мы собираемся создать нового провайдера и привязать его к контейнеру:
pgp artisan make:provider TwilioProvider
// app/Providers/TwilioProvider.php use Illuminate\Support\ServiceProvider; use Twilio\Rest\Client; class TwilioProvider extends ServiceProvider { /** * Bootstrap the application services. * * @return void */ public function boot() {} /** * Register the application services. * * @return void */ public function register() { $this->app->bind('twilio', function() { return new Client(env('TWILIO_SID'), env('TWILIO_TOKEN')); }); } }
Теперь мы можем наконец вернуться к нашему методу sendCode
:
// app/Token.php class Token extends Model { //... public function sendCode() { if (! $this->user) { throw new \Exception("No user attached to this token."); } if (! $this->code) { $this->code = $this->generateCode(); } try { app('twilio')->messages->create($this->user->getPhoneNumber(), ['from' => env('TWILIO_NUMBER'), 'body' => "Your verification code is {$this->code}"]); } catch (\Exception $ex) { return false; //enable to send SMS } return true; } }
Если текущий токен не привязан к пользователю, функция сгенерирует исключение. В противном случае он попытается отправить им SMS.
Наконец приложение готово. Мы проверяем ситуацию, регистрируя нового пользователя и пытаясь войти в систему. Вот небольшая демонстрация:
Полезные ссылки, связанные с 2FA
- https://www.wired.com/2016/06/hey-stop-using-texts-two-factor-authentication/
- https://techcrunch.com/2016/06/10/how-activist-deray-mckessons-twitter-account-was-hacked/
- https://www.twilio.com/blog/2015/01/mobile-passwordless-sms-authentication-part-1-building-the-api-with-laravel-and-twilio.html
- https://www.twilio.com/docs/libraries/php
- https://www.twilio.com/docs/quickstart/php/sms
- https://www.sitepoint.com/2fa-in-laravel-with-google-authenticator-get-secure/
Вывод
Эта статья была кратким введением в интеграцию двухфакторной аутентификации с Twilio с приложением Laravel. Вы также можете продвинуть эту демонстрацию дальше, предоставив пользователям возможность включать и отключать двухфакторную аутентификацию, и вы также можете предложить звонок вместо SMS!
Вы когда-нибудь использовали 2FA для приложения? С какими проблемами вы столкнулись? Это был хороший опыт для ваших пользователей?
Если у вас есть какие-либо вопросы или комментарии по поводу 2FA или Laravel, вы можете опубликовать их ниже!