UserApp.io — это удобный инструмент для управления пользователями и API. Он предоставляет веб-интерфейс для работы с учетными записями пользователей (и многие функции, которые это включает) и API для подключения их к вашему собственному веб-приложению. Цель этого сервиса — сделать управление аутентификацией пользователей более простым и безопасным, не беспокоясь об этом на своем собственном сервере.
Он имеет SDK и различные обертки для многих языков программирования и платформ, и цена является доступной. Да, это идет с ценой, но вы можете начать свободно с довольно много вещей, чтобы поиграть. Я рекомендую проверить их страницу функций, чтобы получить больше информации. Кроме того, очень легко создать учетную запись и поэкспериментировать с созданием пользователей, добавлением свойств в их профили и т. Д., Поэтому я рекомендую также проверить это, если вы этого еще не сделали.
В этой статье мы рассмотрим, как мы можем реализовать механизм аутентификации Symfony2, который использует UserApp.io. Код, который мы пишем, также можно найти в этой небольшой библиотеке, которую я создал (в настоящее время в dev), которую вы можете попробовать. Чтобы установить его в приложение Symfony, просто следуйте инструкциям на GitHub.
Dependecies
Для связи со службой UserApp.io мы будем использовать их библиотеку PHP . Убедитесь, что это требуется в файле composer.json вашего приложения Symfony, как указано на странице GitHub.
Классы аутентификации
Чтобы аутентифицировать пользователей UserApp.io с помощью нашего приложения Symfony, мы создадим несколько классов:
- Класс средства проверки подлинности формы, используемый для выполнения проверки подлинности с помощью API-интерфейса UserApp.io.
- Пользовательский класс User, используемый для представления нашим пользователям информации, полученной из API
- Класс провайдера пользователя, используемый для извлечения пользователей и их преобразования в объекты нашего класса User
- Класс Token, используемый для представления токена аутентификации Symfony
- Класс обработчика выхода из системы, который обеспечивает выход из службы UserApp.io.
- Простой класс исключений, который мы можем выбросить, если у пользователей UserApp.io не установлены какие-либо разрешения (мы конвертируем их в роли Symfony)
Как только мы создадим эти классы, мы объявим некоторые из них как сервисы и будем использовать их в системе безопасности Symfony.
Форма аутентификатора
Во-первых, мы создадим самый важный класс — средство проверки подлинности формы (внутри папки Security/
нашей лучшей практики под названием AppBundle
). Вот код, который я объясню позже:
<?php /** * @file AppBundle\Security\UserAppAuthenticator.php */ namespace AppBundle\Security; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\SimpleFormAuthenticatorInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserProviderInterface; use UserApp\API as UserApp; use UserApp\Exceptions\ServiceException; class UserAppAuthenticator implements SimpleFormAuthenticatorInterface { /** * @var UserApp */ private $userAppClient; public function __construct(UserApp $userAppClient) { $this->userAppClient = $userAppClient; } /** * {@inheritdoc} */ public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) { try { $login = $this->userAppClient->user->login(array( "login" => $token->getUsername(), "password" => $token->getCredentials(), ) ); // Load user from provider based on id $user = $userProvider->loadUserByLoginInfo($login); } catch(ServiceException $exception) { if ($exception->getErrorCode() == 'INVALID_ARGUMENT_LOGIN' || $exception->getErrorCode() == 'INVALID_ARGUMENT_PASSWORD') { throw new AuthenticationException('Invalid username or password'); } if ($exception->getErrorCode() == 'INVALID_ARGUMENT_APP_ID') { throw new AuthenticationException('Invalid app ID'); } } return new UserAppToken( $user, $user->getToken(), $providerKey, $user->getRoles() ); } /** * {@inheritdoc} */ public function supportsToken(TokenInterface $token, $providerKey) { return $token instanceof UserAppToken && $token->getProviderKey() === $providerKey; } /** * {@inheritdoc} */ public function createToken(Request $request, $username, $password, $providerKey) { return new UserAppToken($username, $password, $providerKey); } }
Как видите, мы реализуем SimpleFormAuthenticatorInterface
и, следовательно, имеем 3 метода и конструктор. Последний принимает зависимость в качестве экземпляра клиента UserApp.io (передается с использованием сервисного контейнера, но об этом через минуту).
Этот класс используется Symfony, когда пользователь пытается войти в систему и пройти аутентификацию в приложении. Первое, что происходит, это то, что createToken()
вызывается. Этот метод должен возвращать токен аутентификации, который объединяет введенное имя пользователя и пароль. В нашем случае это будет экземпляр класса UserAppToken
мы определим через минуту.
Затем supportToken()
метод supportToken()
чтобы проверить, поддерживает ли этот класс токен, возвращаемый createToken()
. Здесь мы просто убедитесь, что мы возвращаем true для нашего типа токена.
Наконец, authenticateToken()
вызывается и пытается проверить, действительны ли учетные данные в токене. Здесь и используя PHP-библиотеку UserApp.io, мы пытаемся войти в систему или вызвать исключение аутентификации Symfony, если это не удается. Однако, если аутентификация прошла успешно, ответственный поставщик пользователей используется для создания нашего пользовательского объекта перед созданием и возвратом другого объекта токена на основе последнего.
Мы напишем нашего провайдера сразу после того, как быстро создадим простой класс UserAppToken
.
Класс токенов
<?php /** * @file AppBundle\Security\UserAppToken.php */ namespace AppBundle\Security; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; class UserAppToken extends UsernamePasswordToken { }
Как вы можете видеть, это просто расширение класса UsernamePasswordToken
для более точного именования (поскольку мы храним токен вместо пароля).
Пользователь провайдер
Далее, давайте посмотрим, как аутентификатор работает с провайдером пользователей, поэтому пришло время создать и его:
<?php /** * @file AppBundle\Security\UserAppProvider.php */ namespace AppBundle\Security; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use UserApp\API as UserApp; use UserApp\Exceptions\ServiceException; use AppBundle\Security\Exception\NoUserRoleException; use AppBundle\Security\UserAppUser; class UserAppProvider implements UserProviderInterface { /** * @var UserApp */ private $userAppClient; public function __construct(UserApp $userAppClient) { $this->userAppClient = $userAppClient; } /** * {@inheritdoc} */ public function loadUserByUsername($username) { // Empty for now } /** * {@inheritdoc} */ public function refreshUser(UserInterface $user) { if (!$user instanceof UserAppUser) { throw new UnsupportedUserException( sprintf('Instances of "%s" are not supported.', get_class($user)) ); } try { $api = $this->userAppClient; $api->setOption('token', $user->getToken()); $api->token->heartbeat(); $user->unlock(); } catch (ServiceException $exception) { if ($exception->getErrorCode() == 'INVALID_CREDENTIALS') { throw new AuthenticationException('Invalid credentials'); } if ($exception->getErrorCode() == 'AUTHORIZATION_USER_LOCKED') { $user->lock(); } } return $user; } /** * {@inheritdoc} */ public function supportsClass($class) { return $class === 'AppBundle\Security\UserAppUser'; } /** * * Loads a user from UserApp.io based on a successful login response. * * @param $login * @return UserAppUser * @throws NoUserRoleException */ public function loadUserByLoginInfo($login) { try { $api = $this->userAppClient; $api->setOption('token', $login->token); $users = $api->user->get(); } catch(ServiceException $exception) { if ($exception->getErrorCode() == 'INVALID_ARGUMENT_USER_ID') { throw new UsernameNotFoundException(sprintf('User with the id "%s" not found.', $login->user_id)); } } if (!empty($users)) { return $this->userFromUserApp($users[0], $login->token); } } /** * Creates a UserAppUser from a user response from UserApp.io * * @param $user * @param $token * @return UserAppUser * @throws NoUserRoleException */ private function userFromUserApp($user, $token) { $roles = $this->extractRolesFromPermissions($user); $options = array( 'id' => $user->user_id, 'username' => $user->login, 'token' => $token, 'firstName' => $user->first_name, 'lastName' => $user->last_name, 'email' => $user->email, 'roles' => $roles, 'properties' => $user->properties, 'features' => $user->features, 'permissions' => $user->permissions, 'created' => $user->created_at, 'locked' => !empty($user->locks), 'last_logged_in' => $user->last_login_at, 'last_heartbeat' => time(), ); return new UserAppUser($options); } /** * Extracts the roles from the permissions list of a user * * @param $user * @return array * @throws NoUserRoleException */ private function extractRolesFromPermissions($user) { $permissions = get_object_vars($user->permissions); if (empty($permissions)) { throw new NoUserRoleException('There are no roles set up for your users.'); } $roles = array(); foreach ($permissions as $role => $permission) { if ($permission->value === TRUE) { $roles[] = $role; } } if (empty($roles)) { throw new NoUserRoleException('This user has no roles enabled.'); } return $roles; } }
Подобно классу аутентификатора формы, мы внедряем клиент UserApp.io в этот класс, используя внедрение зависимостей, и реализуем UserProviderInterface
. Последнее требует у нас 3 метода:
-
loadUserByUsername()
— который мы пока оставляем пустым, так как он нам не нужен -
refreshUser()
— которыйrefreshUser()
при каждом аутентифицированном запросе -
supportsClass()
— определяет, работает ли этот пользовательский провайдер с нашим (еще не созданным) пользовательским классом.
Давайте вернемся на секунду к нашему классу аутентификатора и посмотрим, что именно происходит, когда аутентификация с помощью UserApp.io прошла успешно: мы вызываем пользовательский loadUserByLoginInfo()
в классе провайдера пользователя, который берет объект результата успешного входа из API и использует его аутентификацию. токен, чтобы запросить у API вошедший в систему пользовательский объект. Результат UserAppUser
в наш собственный локальный класс UserAppUser
помощью вспомогательных методов userFromUserApp()
и extractRolesFromPermissions()
. Последнее является моей собственной реализацией способа преобразования концепции permissions
в UserApp.io в roles
в Symfony. И мы NoUserRoleException
наше собственное NoUserRoleException
если UserApp.io не настроен с разрешениями для пользователей. Поэтому убедитесь, что ваши пользователи в UserApp.io имеют разрешения, которые вы хотите сопоставить с ролями в Symfony.
Класс исключения — это простое расширение из стандартного PHP \Exception
:
<?php /** * @file AppBundle\Security\Exception\NoUserRoleException.php */ namespace AppBundle\Security\Exception; class NoUserRoleException extends \Exception { }
Возвращаясь к нашему аутентификатору, мы видим, что если аутентификация с помощью UserApp.io прошла успешно, классифицируемый объект UserAppUser
поставщиком пользователя, содержащим всю необходимую информацию о пользователе. Имея этот объект, нам нужно добавить его в новый экземпляр класса UserAppToken
и вернуть его.
Так что в основном это происходит с того момента, как пользователь пытается войти в систему:
- мы создаем токен с предоставленными учетными данными (
createToken()
) - мы пытаемся аутентифицировать учетные данные в этом токене и генерируем исключение аутентификации, если нам не удается
- мы создаем новый токен, содержащий объект пользователя и некоторую другую информацию, если аутентификация прошла успешно
- мы возвращаем этот токен, который Symfony будет использовать для хранения пользователя в сеансе.
Метод refreshUser()
в провайдере пользователя также очень важен. Этот метод отвечает за получение нового экземпляра текущего пользователя, который вошел в систему при каждом обновлении страницы, прошедшей проверку подлинности. Таким образом, всякий раз, когда аутентифицированный пользователь переходит на любую страницу внутри брандмауэра, этот метод запускается. Суть заключается в том, чтобы увлажнять пользовательский объект любыми изменениями в хранилище, которые могли произойти за это время.
Очевидно, нам нужно свести к минимуму вызовы API, но это хорошая возможность увеличить время аутентификации UserApp.io, отправив запрос сердцебиения. По умолчанию (но настраиваемый) каждый токен пользователя, прошедшего проверку подлинности, действителен в течение 60 минут, но при отправке запроса сердцебиения он увеличивается на 20 минут.
Это отличное место для выполнения двух других функций:
- Если токен истек тем временем в UserApp.io, мы получаем исключение со значением
INVALID_CREDENTIALS
поэтому, создав исключение SymfonyAuthenticationException
мы также выходим из системы в Symfony. - Несмотря на то, что запросы сердцебиения сделаны как можно более дешевыми (что означает, что реальные пользовательские данные не извлекаются), статус
locked
пользователя действительно передается обратно в форме исключения. Таким образом, мы можем воспользоваться этой возможностью и пометить объект User как заблокированный.locked
статус можно затем использовать в приложении, например, проверяя его и отказывая в доступе к различным частям, если пользователь заблокирован.
Если вы хотите, вы можете сделать запрос API и обновить объект пользователя с данными из UserApp.io здесь, но я считаю, что это не имеет особого смысла для большинства случаев использования. Данные могут быть обновлены, когда пользователь выходит из системы и возвращается в следующий раз. Но в зависимости от потребностей это легко сделать здесь. Хотя имейте в виду последствия для производительности и стоимость многих вызовов API для UserApp.io.
И в основном это суть нашей логики аутентификации.
Пользовательский класс
Давайте также создадим класс UserAppUser
о UserAppUser
мы говорили ранее:
<?php /** * @file AppBundle\Security\UserAppUser.php */ namespace AppBundle\Security; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Security\Core\User\UserInterface; class UserAppUser implements UserInterface { private $id; private $username; private $token; private $firstName; private $lastName; private $email; private $roles; private $properties; private $features; private $permissions; private $created; private $locked; public function __construct($options) { $resolver = new OptionsResolver(); $this->configureOptions($resolver); $params = $resolver->resolve($options); foreach ($params as $property => $value) { $this->{$property} = $value; } } /** * Configures the class options * * @param $resolver OptionsResolver */ private function configureOptions($resolver) { $resolver->setDefaults(array( 'id' => NULL, 'username' => NULL, 'token' => NULL, 'firstName' => NULL, 'lastName' => NULL, 'email' => NULL, 'roles' => array(), 'properties' => array(), 'features' => array(), 'permissions' => array(), 'created' => NULL, 'locked' => NULL, 'last_logged_in' => NULL, 'last_heartbeat' => NULL, )); $resolver->setRequired(array('id', 'username')); } /** * {@inheritdoc} */ public function getRoles() { return $this->roles; } /** * {@inheritdoc} */ public function getToken() { return $this->token; } /** * {@inheritdoc} */ public function getSalt() { } /** * {@inheritdoc} */ public function getUsername() { return $this->username; } /** * {@inheritdoc} */ public function eraseCredentials() { } /** * {@inheritdoc} */ public function getPassword() { } /** * @return mixed */ public function getId() { return $this->id; } /** * @return array */ public function getProperties() { return $this->properties; } /** * @return mixed */ public function isLocked() { return $this->locked; } /** * Locks the user */ public function lock() { $this->locked = true; } /** * Unlocks the user */ public function unlock() { $this->locked = false; } /** * @return mixed */ public function getFirstName() { return $this->firstName; } /** * @return mixed */ public function getLastName() { return $this->lastName; } /** * @return mixed */ public function getEmail() { return $this->email; } /** * @return mixed */ public function getFeatures() { return $this->features; } /** * @return mixed */ public function getCreated() { return $this->created; } /** * @return mixed */ public function getPermissions() { return $this->permissions; } }
Ничего особенного, мы просто отображаем некоторые данные из UserApp.io и реализуем некоторые методы, требуемые интерфейсом. Кроме того, мы добавили locked/unlocked
флажок.
Выйти
Последний класс, который нам нужно создать, это тот, который касается выхода пользователя из UserApp.io, когда он выходит из Symfony.
<?php /** * @file AppBundle\Security\UserAppLogout.php */ namespace AppBundle\Security; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface; use UserApp\API as UserApp; use UserApp\Exceptions\ServiceException; class UserAppLogout implements LogoutHandlerInterface { /** * @var UserApp */ private $userAppClient; public function __construct(UserApp $userAppClient) { $this->userAppClient = $userAppClient; } /** * {@inheritdoc} */ public function logout(Request $request, Response $response, TokenInterface $token) { $api = $this->userAppClient; $user = $token->getUser(); $api->setOption('token', $user->getToken()); try { $api->user->logout(); } catch (ServiceException $exception) { // Empty for now, error probably caused by user not being authenticated which means // user is logged out already. } } }
из<?php /** * @file AppBundle\Security\UserAppLogout.php */ namespace AppBundle\Security; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface; use UserApp\API as UserApp; use UserApp\Exceptions\ServiceException; class UserAppLogout implements LogoutHandlerInterface { /** * @var UserApp */ private $userAppClient; public function __construct(UserApp $userAppClient) { $this->userAppClient = $userAppClient; } /** * {@inheritdoc} */ public function logout(Request $request, Response $response, TokenInterface $token) { $api = $this->userAppClient; $user = $token->getUser(); $api->setOption('token', $user->getToken()); try { $api->user->logout(); } catch (ServiceException $exception) { // Empty for now, error probably caused by user not being authenticated which means // user is logged out already. } } }
Здесь мы снова внедряем PHP-клиент UserApp.io и, поскольку мы реализуем LogoutHandlerInterface
нам нужен метод logout()
. Все, что мы делаем в этом, — это выход пользователя из UserApp.io, если он все еще зарегистрирован.
Проводить все
Теперь, когда у нас есть наши классы, пришло время объявить их как сервисы и использовать их в нашей системе аутентификации. Вот наши объявления службы на основе YML:
user_app_client: class: UserApp\API arguments: ["%userapp_id%"] user_app_authenticator: class: AppBundle\Security\UserAppAuthenticator arguments: ["@user_app_client"] user_app_provider: class: AppBundle\Security\UserAppProvider arguments: ["@user_app_client"] user_app_logout: class: AppBundle\Security\UserAppLogout arguments: ["@user_app_client"]
Первая — это PHP-библиотека UserApp.io, в которую мы передаем идентификатор нашего приложения в виде ссылки на параметр. Вам понадобится параметр userapp_id
с идентификатором приложения UserApp.io.
Остальными тремя являются классы проверки подлинности форм, провайдера пользователей и выхода из системы, которые мы написали ранее. И, как вы помните, каждый принимает один параметр в своем конструкторе в виде клиента UserApp.io, определенного как первый сервис.
Затем пришло время использовать эти сервисы в нашей системе безопасности, поэтому отредактируйте файл security.yml
и сделайте следующее:
-
Под ключом
providers
добавьте следующее:user_app: id: user_app_provider
Здесь мы указываем, что у нашего приложения есть также этот пользовательский поставщик, поэтому он может его использовать.
-
Под ключом
firewall
добавьте следующее:
secured_area: pattern: ^/secured/ simple_form: authenticator: user_app_authenticator check_path: security_check login_path: login logout: path: logout handlers: [user_app_logout] target: _home anonymous: ~
Здесь происходит то, что мы определяем простую безопасную область, которая использует тип аутентификации simple_form
с нашим аутентификатором. Под ключом logout
мы добавляем вызываемый обработчик (наш класс UserAppLogout
определен как сервис). Остальное — обычная настройка безопасности Symfony, поэтому убедитесь, что у вас есть форма входа в систему, отображаемая на маршруте login
, и т. Д. Ознакомьтесь с документацией по этому вопросу для получения дополнительной информации.
И это все. Используя аутентификацию simple_form
с нашим пользовательским simple_form
проверки подлинности форм и провайдером пользователей (вместе с необязательным обработчиком выхода из системы), мы реализовали наш собственный механизм аутентификации Symfony на основе UserApp.io.
Вывод
В этой статье мы увидели, как реализовать пользовательскую проверку подлинности формы Symfony с использованием службы UserApp.io и API в качестве поставщика пользователя. Мы прошли довольно много кода, что означало очень краткое объяснение самого кода. Вместо этого я попытался объяснить процесс аутентификации с помощью Symfony, создав собственное решение, которое учитывает способы взаимодействия с UserApp.io.
Если вы последовали этому примеру и внедрили этот метод в свой пакет и хотите использовать его следующим образом, продолжайте. У вас также есть возможность использовать созданную мной библиотеку, которая имеет очень быструю и простую настройку, описанную на странице GitHub. Я рекомендую последнее, потому что я планирую разработать и поддерживать его, чтобы вы всегда могли получить обновленную версию, если будут удалены какие-либо ошибки или добавлены функции (надеюсь, что не наоборот).
Если вы хотите внести свой вклад в это, вы очень рады. Я также признателен, если вы сообщите мне, если у вас возникнут какие-либо проблемы или вы найдете более эффективные способы достижения аналогичных целей.