Статьи

Аутентификация пользователя в Symfony2 с UserApp.io

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 и вернуть его.

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

  1. мы создаем токен с предоставленными учетными данными ( createToken() )
  2. мы пытаемся аутентифицировать учетные данные в этом токене и генерируем исключение аутентификации, если нам не удается
  3. мы создаем новый токен, содержащий объект пользователя и некоторую другую информацию, если аутентификация прошла успешно
  4. мы возвращаем этот токен, который Symfony будет использовать для хранения пользователя в сеансе.

Метод refreshUser() в провайдере пользователя также очень важен. Этот метод отвечает за получение нового экземпляра текущего пользователя, который вошел в систему при каждом обновлении страницы, прошедшей проверку подлинности. Таким образом, всякий раз, когда аутентифицированный пользователь переходит на любую страницу внутри брандмауэра, этот метод запускается. Суть заключается в том, чтобы увлажнять пользовательский объект любыми изменениями в хранилище, которые могли произойти за это время.

Очевидно, нам нужно свести к минимуму вызовы API, но это хорошая возможность увеличить время аутентификации UserApp.io, отправив запрос сердцебиения. По умолчанию (но настраиваемый) каждый токен пользователя, прошедшего проверку подлинности, действителен в течение 60 минут, но при отправке запроса сердцебиения он увеличивается на 20 минут.

Это отличное место для выполнения двух других функций:

  1. Если токен истек тем временем в UserApp.io, мы получаем исключение со значением INVALID_CREDENTIALS поэтому, создав исключение Symfony AuthenticationException мы также выходим из системы в Symfony.
  2. Несмотря на то, что запросы сердцебиения сделаны как можно более дешевыми (что означает, что реальные пользовательские данные не извлекаются), статус 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 и сделайте следующее:

  1. Под ключом providers добавьте следующее:

     user_app: id: user_app_provider 

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

  2. Под ключом 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. Я рекомендую последнее, потому что я планирую разработать и поддерживать его, чтобы вы всегда могли получить обновленную версию, если будут удалены какие-либо ошибки или добавлены функции (надеюсь, что не наоборот).

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