От автора: в этой статье мы рассмотрим систему аутентификации в фреймворке Laravel. Цель статьи – создать кастомный Guard аутентификации, расширив базовую систему аутентификации. Laravel предоставляет очень надежную систему аутентификации в ядре, что упрощает реализацию стандартной аутентификации. На деле, для установки системы аутентификации необходимо запустить пару выученных команд.
Сама система спроектирована таким образом, что вы можете расширить ее и вставить в нее свои кастомные адаптеры аутентификации. Это мы подробно обсудим в этой статье. Прежде чем мы с головой погрузимся в реализацию кастомного Guard аутентификации, давайте обсудим базовые элементы, благодаря которым работает аутентификация Laravel – Guard и провайдеры.
Элементы ядра: Guard и провайдеры
Система аутентификации Laravel состоит из двух элементов – Guard и провайдеры.
Guard
Представьте Guard, как способ доставки логики, используемой для идентификации прошедших проверку пользователей. В ядре Laravel есть разные Guard – сессия и токен. Guard сессии обслуживает состояние пользователя во всех запросах через куки, а Guard токен проверяет подлинность пользователя через токен в каждом запросе.
Guard определяет логику аутентификации, и необязательно, чтобы он постоянно получал валидные данные авторизации с back end. Можно сделать Guard, который просто проверяет наличие определенных данных в заголовках запроса и на их основе проводит аутентификацию.
Ниже в статье мы создадим Guard проверки определенных JSON параметров в заголовках запроса, который будет получать валидного пользователя с MongoDB back end.
Провайдеры
Если Guard определяет логику аутентификации, то провайдер аутентификации получает пользователя с хранилища back end. Если Guard требует, чтобы пользователь проверялся через back end хранилище, то реализация получения пользователя переходит в провайдер аутентификации.
Laravel идет с двумя стандартными провайдерами аутентификации — Database и Eloquent. Провайдер аутентификации Database работает с простым вытягиванием данных авторизации пользователя из хранилища back end, а Eloquent дает абстрактный слой.
В нашем примере мы реализуем провайдер аутентификации MongoDB, который будет получать данные авторизации пользователя через MongoDB back end.
Это было базовое представление Guard и провайдеров в системе аутентификации Laravel. Начиная со следующего раздела, мы будем разрабатывать кастомные Guard аутентификации и провайдер!
Быстрая настройка файлов
Давайте быстро пробежимся по файлам, которые нам необходимо создать в этой статье.
config/auth.php: файл настроек аутентификации, в который мы добавим точку входа в кастомный Guard.
config/mongo.php: файл для настроек MongoDB.
app/Services/Contracts/NosqlServiceInterface.php: интерфейс, который реализует наш кастомный класс Mongo database.
app/Database/MongoDatabase.php: главный класс базы данных, взаимодействующий с MongoDB.
app/Models/Auth/User.php: класс модели User, реализующий договор Authenticable.
app/Extensions/MongoUserProvider.php: реализация провайдера аутентификации.
app/Services/Auth/JsonGuard.php: реализация Guard драйвера аутентификации.
app/Providers/AuthServiceProvider.php: файл, с помощью которого мы будем добавлять привязки к сервис контейнерам.
app/Http/Controllers/MongoController.php: файл демо контроллера, с помощью которого мы будем тестировать наш кастомный Guard.
Не пугайтесь, что список файлов пока что вам ничего не говорит, мы постепенно все разберем.
Реализация
В этом разделе мы создадим необходимые файлы. Первым делом нам необходимо информировать Laravel о кастомном Guard. Скопируйте код ниже в файл config/auth.php.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
... ... 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'token', 'provider' => 'users', ], 'custom' => [ 'driver' => 'json', 'provider' => 'mongo', ], ], ... ... |
Как видите, мы добавили кастомный Guard под ключом custom. Далее необходимо добавить соответствующий провайдер в раздел providers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
... ... 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\User::class, ], 'mongo' => [ 'driver' => 'mongo' ], // 'users' => [ // 'driver' => 'database', // 'table' => 'users', // ], ], ... ... |
Провайдер добавлен под ключом mongo. Изменим стандартный Guard аутентификации с web на custom.
1 2 3 4 5 6 7 8 |
... ... 'defaults' => [ 'guard' => 'custom', 'passwords' => 'users', ], ... ... |
Конечно, пока что ничего не работает, так как мы не создали необходимые файлы. Этим мы и займемся в следующих разделах.
Создание MongoDB Driver
В этом разделе мы создадим необходимые файлы, которые будут общаться с объектом MongoDB. Сначала создадим файл настроек config/mongo.php, в котором будут храниться стандартные настройки подключения MongoDB.
1 2 3 4 5 6 7 8 |
<?php return [ 'defaults' => [ 'host' => '{HOST_IP}', 'port' => '{HOST_PORT}', 'database' => '{DB_NAME}' ] ]; |
Данные необходимо заменить на свои. Вместо класса, взаимодействующего с MongoDB мы создадим интерфейс.
Плюс интерфейса в том, что он дает абстрактность, которой разработчик должен придерживаться при его реализации. Также нашу реализацию MongoDB можно легко заменить на другую NoSQL реализацию без необходимости.
Создайте файл интерфейса app/Services/Contracts/NosqlServiceInterface.php и добавьте в него код ниже.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
<?php // app/Services/Contracts/NosqlServiceInterface.php namespace App\Services\Contracts; Interface NosqlServiceInterface { /** * Create a Document * * @param string $collection Collection/Table Name * @param array $document Document * @return boolean */ public function create($collection, Array $document); /** * Update a Document * * @param string $collection Collection/Table Name * @param mix $id Primary Id * @param array $document Document * @return boolean */ public function update($collection, $id, Array $document); /** * Delete a Document * * @param string $collection Collection/Table Name * @param mix $id Primary Id * @return boolean */ public function delete($collection, $id); /** * Search Document(s) * * @param string $collection Collection/Table Name * @param array $criteria Key-value criteria * @return array */ public function find($collection, Array $criteria); } |
Простой интерфейс, определяющий CRUD методы, которые должен определить класс, реализующий этот интерфейс. Теперь создадим класс app/Database/MongoDatabase.php.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<?php // app/Database/MongoDatabase.php namespace App\Database; use App\Services\Contracts\NosqlServiceInterface; class MongoDatabase implements NosqlServiceInterface { private $connection; private $database; public function __construct($host, $port, $database) { $this->connection = new MongoClient( "mongodb://{$host}:{$port}" ); $this->database = $this->connection->{$database}; } /** * @see \App\Services\Contracts\NosqlServiceInterface::find() */ public function find($collection, Array $criteria) { return $this->database->{$collection}->findOne($criteria); } public function create($collection, Array $document) {} public function update($collection, $id, Array $document) {} public function delete($collection, $id) {} } |
Предполагаю, что вы уже установили MongoDB и соответствующее расширение MongoDB PHP.
Метод __construct создает объект класса MongoClient с необходимыми параметрами. Другой важный метод, нужный нам – find. Он получает запись на основе переданных аргументов. Мы реализовали драйвер MongoDB и постарались сохранить его простоту.
Создание модели User
Придерживаясь стандартов системы аутентификации, нам необходимо реализовать модель User, которая должна реализовывать контракт Illuminate\Contracts\Auth\Authenticatable. Создайте файл app/Models/Auth/User.php со следующим кодом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
<?php // app/Models/Auth/User.php namespace App\Models\Auth; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use App\Services\Contracts\NosqlServiceInterface; class User implements AuthenticatableContract { private $conn; private $username; private $password; protected $rememberTokenName = 'remember_token'; public function __construct(NosqlServiceInterface $conn) { $this->conn = $conn; } /** * Fetch user by Credentials * * @param array $credentials * @return Illuminate\Contracts\Auth\Authenticatable */ public function fetchUserByCredentials(Array $credentials) { $arr_user = $this->conn->find('users', ['username' => $credentials['username']]); if (! is_null($arr_user)) { $this->username = $arr_user['username']; $this->password = $arr_user['password']; } return $this; } /** * {@inheritDoc} * @see \Illuminate\Contracts\Auth\Authenticatable::getAuthIdentifierName() */ public function getAuthIdentifierName() { return "username"; } /** * {@inheritDoc} * @see \Illuminate\Contracts\Auth\Authenticatable::getAuthIdentifier() */ public function getAuthIdentifier() { return $this->{$this->getAuthIdentifierName()}; } /** * {@inheritDoc} * @see \Illuminate\Contracts\Auth\Authenticatable::getAuthPassword() */ public function getAuthPassword() { return $this->password; } /** * {@inheritDoc} * @see \Illuminate\Contracts\Auth\Authenticatable::getRememberToken() */ public function getRememberToken() { if (! empty($this->getRememberTokenName())) { return $this->{$this->getRememberTokenName()}; } } /** * {@inheritDoc} * @see \Illuminate\Contracts\Auth\Authenticatable::setRememberToken() */ public function setRememberToken($value) { if (! empty($this->getRememberTokenName())) { $this->{$this->getRememberTokenName()} = $value; } } /** * {@inheritDoc} * @see \Illuminate\Contracts\Auth\Authenticatable::getRememberTokenName() */ public function getRememberTokenName() { return $this->rememberTokenName; } } |
Возможно, вы уже заметили, что App\Models\Auth\User реализует контракт Illuminate\Contracts\Auth\Authenticatable.
Большинство методов, реализованных в нашем классе, говорят сами за себя. Мы определили метод fetchUserByCredentials, который вытягивает пользователя с back end. В нашем случае это будет класс MongoDatabase, именно к нему мы будем обращаться за необходимой информацией. Мы создали реализацию модели User.
Создание провайдера аутентификации
Мы уже говорили ранее, что система аутентификации Laravel состоит из двух элементов – Guard и провайдеры. В этом разделе мы создадим провайдер аутентификации, который будет вытягивать пользователя с back end. Создайте файл app/Extensions/MongoUserProvider.php, как показано ниже.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
<?php // app/Extensions/MongoUserProvider.php namespace App\Extensions; use Illuminate\Support\Str; use Illuminate\Contracts\Auth\UserProvider; use Illuminate\Contracts\Auth\Authenticatable; class MongoUserProvider implements UserProvider { /** * The Mongo User Model */ private $model; /** * Create a new mongo user provider. * * @return \Illuminate\Contracts\Auth\Authenticatable|null * @return void */ public function __construct(\App\Models\Auth\User $userModel) { $this->model = $userModel; } /** * Retrieve a user by the given credentials. * * @param array $credentials * @return \Illuminate\Contracts\Auth\Authenticatable|null */ public function retrieveByCredentials(array $credentials) { if (empty($credentials)) { return; } $user = $this->model->fetchUserByCredentials(['username' => $credentials['username']]); return $user; } /** * Validate a user against the given credentials. * * @param \Illuminate\Contracts\Auth\Authenticatable $user * @param array $credentials Request credentials * @return bool */ public function validateCredentials(Authenticatable $user, Array $credentials) { return ($credentials['username'] == $user->getAuthIdentifier() && md5($credentials['password']) == $user->getAuthPassword()); } public function retrieveById($identifier) {} public function retrieveByToken($identifier, $token) {} public function updateRememberToken(Authenticatable $user, $token) {} } |
Необходимо, чтобы кастомный провайдер реализовывал контракт Illuminate\Contracts\Auth\UserProvider. Файл определяет два важных метода retrieveByCredentials и validateCredentials.
Метод retrieveByCredentials используется для получения данных авторизации пользователя через модель User, о которой мы говорили в предыдущем разделе. Метод validateCredentials проверяет пользователя по заданному набору данных.
Мы создали кастомный провайдер аутентификации. В следующем разделе мы перейдем к созданию Guard, который будет взаимодействовать с провайдером аутентификации MongoUserProvider.
Создание Guard аутентификации
Как мы уже говорили ранее, Guard в системе аутентификации Laravel определяет логику аутентификации пользователя. Мы будем проверять наличие параметра запроса jsondata. В нем должна быть JSON строка с данными авторизации.
В этом разделе мы создадим Guard, который будет общаться с провайдером аутентификации, созданном в предыдущем разделе. Создайте файл app/Services/Auth/JsonGuard.php со следующим кодом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
<?php // app/Services/Auth/JsonGuard.php namespace App\Services\Auth; use Illuminate\Http\Request; use Illuminate\Contracts\Auth\Guard; use Illuminate\Contracts\Auth\UserProvider; use GuzzleHttp\json_decode; use phpDocumentor\Reflection\Types\Array_; use Illuminate\Contracts\Auth\Authenticatable; class JsonGuard implements Guard { protected $request; protected $provider; protected $user; /** * Create a new authentication guard. * * @param \Illuminate\Contracts\Auth\UserProvider $provider * @param \Illuminate\Http\Request $request * @return void */ public function __construct(UserProvider $provider, Request $request) { $this->request = $request; $this->provider = $provider; $this->user = NULL; } /** * Determine if the current user is authenticated. * * @return bool */ public function check() { return ! is_null($this->user()); } /** * Determine if the current user is a guest. * * @return bool */ public function guest() { return ! $this->check(); } /** * Get the currently authenticated user. * * @return \Illuminate\Contracts\Auth\Authenticatable|null */ public function user() { if (! is_null($this->user)) { return $this->user; } } /** * Get the JSON params from the current request * * @return string */ public function getJsonParams() { $jsondata = $this->request->query('jsondata'); return (!empty($jsondata) ? json_decode($jsondata, TRUE) : NULL); } /** * Get the ID for the currently authenticated user. * * @return string|null */ public function id() { if ($user = $this->user()) { return $this->user()->getAuthIdentifier(); } } /** * Validate a user's credentials. * * @return bool */ public function validate(Array $credentials=[]) { if (empty($credentials['username']) || empty($credentials['password'])) { if (!$credentials=$this->getJsonParams()) { return false; } } $user = $this->provider->retrieveByCredentials($credentials); if (! is_null($user) && $this->provider->validateCredentials($user, $credentials)) { $this->setUser($user); return true; } else { return false; } } /** * Set the current user. * * @param Array $user User info * @return void */ public function setUser(Authenticatable $user) { $this->user = $user; return $this; } } |
Прежде всего, нашему классу необходимо реализовывать интерфейс Illuminate\Contracts\Auth\Guard. Поэтому необходимо определить все методы, объявленные в интерфейсе.
Важно отметить тот факт, что функция __construct требует реализации Illuminate\Contracts\Auth\UserProvider. Мы передадим объект App\Extensions\MongoUserProvider.
Далее есть функция getJsonParams, получающая данные авторизации пользователя из параметра jsondata запроса. Ожидается строка JSON с данными авторизации пользователя, поэтому мы декодируем JSON данные с помощью функции json_decode.
В функции валидации первое, что мы проверяем – существование аргумента $credentials. Если его нет, вызывается метод getJsonParams для получения данных авторизации пользователя из параметров запроса.
Далее мы вызываем метод retrieveByCredentials провайдера MongoUserProvider, который получает пользователя из базы данных MongoDB с back end. И наконец, метод validateCredentials провайдера MongoUserProvider валидирует пользователя.
Это была реализация кастомного Guard. В следующем разделе говорится, как связать все эти части вместе и сформировать систему аутентификации.
Собираем все вместе
Мы разработали все элементы кастомного Guard аутентификации, которые должны дать нам новую систему аутентификации. Однако прямо вот так она не заработает. Ее необходимо зарегистрировать в привязках сервис контейнера Laravel.
Откройте файл app/Providers/AuthServiceProvider.php. С его помощью можно добавить привязки сервис контейнера аутентификации. Если в нем нет кастомных изменений, можете просто заменить код на код ниже.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
<?php // app/Providers/AuthServiceProvider.php namespace App\Providers; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Gate; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use App\Services\Auth\JsonGuard; use App\Extensions\MongoUserProvider; use App\Database\MongoDatabase; use App\Models\Auth\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Config; class AuthServiceProvider extends ServiceProvider { /** * The policy mappings for the application. * * @var array */ protected $policies = [ 'App\Model' => 'App\Policies\ModelPolicy', ]; /** * Register any authentication / authorization services. * * @return void */ public function boot() { $this->registerPolicies(); $this->app->bind('App\Database\MongoDatabase', function ($app) { return new MongoDatabase(config('mongo.defaults.host'), config('mongo.defaults.port'), config('mongo.defaults.database')); }); $this->app->bind('App\Models\Auth\User', function ($app) { return new User($app->make('App\Database\MongoDatabase')); }); // add custom guard provider Auth::provider('mongo', function ($app, array $config) { return new MongoUserProvider($app->make('App\Models\Auth\User')); }); // add custom guard Auth::extend('json', function ($app, $name, array $config) { return new JsonGuard(Auth::createUserProvider($config['provider']), $app->make('request')); }); } public function register() { $this->app->bind( 'App\Services\Contracts\NosqlServiceInterface', 'App\Database\MongoDatabase' ); } } |
Рассмотрим метод boot, в котором хранится большая часть привязок провайдера. Для начала создадим привязки для App\Database\MongoDatabase и App\Models\Auth\User.
1 2 3 4 5 6 7 |
$this->app->bind('App\Database\MongoDatabase', function ($app) { return new MongoDatabase(config('mongo.defaults.host'), config('mongo.defaults.port'), config('mongo.defaults.database')); }); $this->app->bind('App\Models\Auth\User', function ($app) { return new User($app->make('App\Database\MongoDatabase')); }); |
Пришло время вставить наш кастомный Guard в систему аутентификации Laravel.
С помощью провайдер метода Auth Facade мы добавили наш кастомный провайдер аутентификации под ключ mongo. Вспоминайте, этот ключ отражает настройки, которые мы ранее вносили в файл auth.php.
1 2 3 |
Auth::provider('mongo', function ($app, array $config) { return new MongoUserProvider($app->make('App\Models\Auth\User')); }); |
Точно так же мы вставим наша реализацию кастомного Guard с помощью метода extend Auth façade.
1 2 3 |
Auth::extend('json', function ($app, $name, array $config) { return new JsonGuard(Auth::createUserProvider($config['provider']), $app->make('request')); }); |
Далее идет метод register, с помощью которого мы привязали интерфейс App\Services\Contracts\NosqlServiceInterface к реализации App\Database\MongoDatabase.
1 2 3 4 |
$this->app->bind( 'App\Services\Contracts\NosqlServiceInterface', 'App\Database\MongoDatabase' ); |
Теперь когда понадобится разрешить зависимость App\Services\Contracts\NosqlServiceInterface, Laravel ответит реализацией адаптера App\Database\MongoDatabase.
Преимущество этого подхода в том, что реализацию можно легко заменить на другую. Например, кто-то захотел заменить реализацию App\Database\MongoDatabase на адаптер CouchDB. Для этого ему необходимо лишь добавить соответствующую привязку в метод register.
Сервис провайдер в вашем распоряжении. Сейчас у нас есть все необходимое для тестирования кастомной реализации Guard. Остался раздел, подводящий итоги.
Это работает?
Вы проделали сложную работу по созданию своего первого кастомного Guard аутентификации. Пора попробовать его в действии. Быстро создадим простой файл контроллера app/Http/Controllers/MongoController.php.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php // app/Http/Controllers/MongoController.php namespace App\Http\Controllers; use App\Http\Controllers\Controller; use Illuminate\Contracts\Auth\Guard; class MongoController extends Controller { public function login(Guard $auth_guard) { if ($auth_guard->validate()) { // get the current authenticated user $user = $auth_guard->user(); echo 'Success!'; } else { echo 'Not authorized to access this page!'; } } } |
Подробно разберем зависимость метода login, который требует реализацию Guard Illuminate\Contracts\Auth\Guard. В файле auth.php по умолчанию мы задали Guard custom, поэтому вставляться будем App\Services\Auth\JsonGuard!
Далее мы вызвали метод validate класса App\Services\Auth\JsonGuard, который запускает серию вызовов методов:
вызывается метод retrieveByCredentials класса App\Extensions\MongoUserProvider
метод retrieveByCredentials вызывает метод fetchUserByCredentials класса App\Models\Auth\User
метод fetchUserByCredentials вызывает метод find класса App\Database\MongoDatabase и получает данные авторизации пользователя
метод find класса App\Database\MongoDatabase возвращает ответ!
Если все отработает, как ожидается, мы должны получить аутентифицированного пользователя через вызов метода user нашего Guard.
Для доступа к контроллеру необходимо добавить соответствующий роут в файл routes/web.php.
1 |
Route::get('/custom/mongo/login', 'MongoController@login'); |
Попробуйте открыть //your-laravel-site/custom/mongo/login, не передавая параметры. Должно отобразиться сообщение «not authorized».
Если открыть что-то типа //your-laravel-site/custom/mongo/login?jsondata={«username»:»admin»,»password»:»admin»}, должно вернуться успешное сообщение, если пользователь есть в базе данных.
Заметьте, что это лишь демонстрация возможностей кастомного Guard. Такие вещи, как логин, необходимо делать с защитой от дурака. Я же показал лишь общий взгляд на процесс аутентификации. Вы сами должны создать надежную и безопасную систему для своего приложения.
Наше приключение заканчивается, вскоре вернусь с новой полезной статьей, надеюсь. Хотите спросить что-то по какой-либо теме, пишите!
Заключение
Фреймворк Laravel предоставляет надежную систему аутентификации в ядре, которую можно расширить кастомной системой. Темой сегодняшней статьи стала реализация кастомного Guard и его вставка в процесс аутентификации Laravel.
В процессе мы разработали систему, аутентифицирующую пользователя по JSON в запросе и проверяющую эти данные с базой данных MongoDB. Для этого мы создали кастомный Guard и провайдер.
Надеюсь, этот пример показал вам общий взгляд на аутентификацию в Laravel, и вы теперь лучше понимаете ее внутреннее устройство.
Автор: Sajal Soni
Источник: //code.tutsplus.com/
Редакция: Команда webformyself.