Интеграция Аутентификации Symfony2 И Трекера Jira

Привет, Хабросообщество.

В этой статье я хочу рассказать вам, как можно подружить знаменитый фреймворк Symfony2 и не менее известный трекер Jira.



Зачем связывать Jira и Symfony2?

В компании, где я работаю, возникла необходимость подключить систему поддержки и трекер задач через API, чтобы запросы от клиентов можно было легко конвертировать в тикеты.

Основной проблемой, которая встала на нашем пути, была интеграция аутентификации Jira (использовался механизм «Базовая аутентификация») и системы безопасности Symfony2. Чтобы понять механизмы аутентификации и авторизации фреймворка, необходимо ознакомиться с официальной документацией: http://symfony.com/doc/current/book/security.html .



Что нужно для создания нового типа авторизации в Symfony2?

  1. Токен, который будет хранить вводимые пользователем данные во время аутентификации.

  2. Прослушиватель, необходимый для проверки аутентификации пользователя.

  3. Поставщик, который напрямую реализует аутентификацию через Jira.
  4. Поставщик пользователя, который будет запрошен Symfony2 Security для получения информации о пользователе.

  5. Factory, которая зарегистрирует новый метод аутентификации и авторизации.



Создать токен

Чтобы хранить информацию, введенную пользователями во время аутентификации, и использовать ее позже, Symfony использует токены, наследуемые от класса AbstractToken. В рассматриваемой задаче необходимо хранить 2 поля — логин и пароль пользователя, на основании которых будет проверяться авторизация в Jira. Код реализации класса токена показан ниже.

  
  
  
  
  
  
  
  
  
  
  
   

<Эphp namespace DG\JiraAuthBundle\Security\Authentication\Token; use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; class JiraToken extends AbstractToken { protected $jira_login; protected $jira_password; public function __construct(array $roles = array('ROLE_USER')){ parent::__construct($roles); $this->setAuthenticated(count($roles) > 0); } public function getJiraLogin(){ return $this->jira_login; } public function setJiraLogin($jira_login){ $this->jira_login = $jira_login; } public function getJiraPassword(){ return $this->jira_password; } public function setJiraPassword($jira_password){ $this->jira_password = $jira_password; } public function serialize() { return serialize(array($this->jira_login, $this->jira_password, parent::serialize())); } public function unserialize($serialized) { list($this->jira_login, $this->jira_password, $parent_data) = unserialize($serialized); parent::unserialize($parent_data); } public function getCredentials(){ return ''; } }



Реализация прослушивателя

Теперь, когда у нас есть сохраненные пользовательские данные, можно проверить их правильность.

Если данные устарели, необходимо сообщить об этом фреймворку.

Для этого вам необходимо реализовать Listener, унаследованный от AbstractAuthenticationListener.

<Эphp namespace DG\JiraAuthBundle\Security\Firewall; use DG\JiraAuthBundle\Security\Authentication\Token\JiraToken; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\SecurityContextInterface; use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener; class JiraListener extends AbstractAuthenticationListener { protected function attemptAuthentication(Request $request){ if ($this->options['post_only'] && 'post' !== strtolower($request->getMethod())) { if (null !== $this->logger) { $this->logger->debug(sprintf('Authentication method not supported: %s.', $request->getMethod())); } return null; } $username = trim($request->get($this->options['username_parameter'], null, true)); $password = $request->get($this->options['password_parameter'], null, true); $request->getSession()->set(SecurityContextInterface::LAST_USERNAME, $username); $request->getSession()->set('jira_auth', base64_encode($username.':'.

$password)); $token = new JiraToken(); $token->setJiraLogin($username); $token->setJiraPassword($password); return $this->authenticationManager->authenticate($token); } }



Авторизация в Jira. Поставщик

Пришло время самого главного — непосредственной отправки данных в Jira. Для работы с остальными API трекера был написан простой класс, подключаемый как сервис.

Библиотека Buzz используется для работы с Jira API.

<Эphp namespace DG\JiraAuthBundle\Jira; use Buzz\Message; use Buzz\Client\Curl; class JiraRest { private $jiraUrl = ''; public function __construct($jiraUrl){ $this->jiraUrl = $jiraUrl; } public function getUserInfo($username, $password){ $request = new Message\Request( 'GET', '/rest/api/2/userЭusername=' .

$username, $this->jiraUrl ); $request->addHeader('Authorization: Basic ' .

base64_encode($username .

':' .

$password) ); $request->addHeader('Content-Type: application/json'); $response = new Message\Response(); $client = new Curl(); $client->setTimeout(10); $client->send($request, $response); return $response; } }

Поставщик должен реализовать интерфейс AuthenticationProviderInterface и выглядит следующим образом:

<Эphp namespace DG\JiraAuthBundle\Security\Authentication\Provider; use DG\JiraAuthBundle\Entity\User; use DG\JiraAuthBundle\Jira\JiraRest; use DG\JiraAuthBundle\Security\Authentication\Token\JiraToken; use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserProviderInterface; class JiraProvider implements AuthenticationProviderInterface { private $userProvider; private $jiraRest; public function __construct(UserProviderInterface $userProvider, $providerKey, JiraRest $jiraRest) { $this->userProvider = $userProvider; $this->jiraRest = $jiraRest; } public function supports(TokenInterface $token) { return $token instanceof JiraToken; } public function authenticate(TokenInterface $token) { $user = $this->checkUserAuthentication($token); $token->setUser($user); return $token; } public function checkUserAuthentication(JiraToken $token){ $response = $this->jiraRest->getUserInfo($token->getJiraLogin(), $token->getJiraPassword()); if(!in_array('HTTP/1.1 200 OK', $response->getHeaders())){ throw new AuthenticationException( 'Incorrect email and/or password' ); } $userInfo = json_decode($response->getContent()); $user = new User(); $user->setUsername($userInfo->name); $user->setBase64Hash(base64_encode($token->getJiraLogin() .

':' .

$token->getJiraPassword())); $user->setEmail($userInfo->emailAddress); $user->addRole('ROLE_USER'); return $user; } }

Как видно из реализации, пользовательские данные хранятся в сущности User. Это можно не делать, чтобы Doctrine не создавала лишнюю таблицу в базе данных, но в дальнейшем вы можете добавить в эту таблицу информацию о пользователях из Jira, чтобы обезопасить себя от временной недоступности трекера.

Такая «страховка» выходит за рамки данной статьи, но может оказаться весьма полезной.



Предоставление информации об авторизованном пользователе

Система безопасности в рамках запрашивает информацию о пользователе для проверки авторизации.

Понятно, что такая информация находится в Jira, поэтому мы должны получить ее от трекера.

Кешировать ответы от Jira, конечно, можно, но пока мы это учитывать не будем.

Код провайдера приведен ниже.



<Эphp namespace DG\JiraAuthBundle\User; use DG\JiraAuthBundle\Entity\User; use DG\JiraAuthBundle\Jira\JiraRest; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\SecurityContextInterface; class JiraUserProvider implements UserProviderInterface { private $jiraRest; public function __construct(JiraRest $jiraRest){ $this->jiraRest = $jiraRest; } public function loadUserByUsername($username) { } public function refreshUser(UserInterface $user) { if (!$user instanceof User) { throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user))); } $decodedUserData = base64_decode($user->getBase64Hash()); list($username, $password) = explode(':', $decodedUserData); $userInfoResponse = $this->jiraRest->getUserInfo($username, $password); $userInfo = json_decode($userInfoResponse->getContent()); $user = new User(); $user->setUsername($user->getUsername()); $user->setEmail($userInfo->emailAddress); $user->setBase64Hash($user->getBase64Hash()); $user->addRole('ROLE_USER'); return $user; } public function supportsClass($class) { return $class === 'DG\JiraAuthBundle\Entity\User'; } }



Заполнение конфигурации

Чтобы использовать созданные классы, необходимо прописать их в конфигурации как сервисы.

Пример Services.yml приведен ниже.

Отмечу, что параметр jira_url должен быть определен в файле options.yml и содержать URL-адрес Jira.

parameters: dg_jira_auth.user_provider.class: DG\JiraAuthBundle\User\JiraUserProvider dg_jira_auth.listener.class: DG\JiraAuthBundle\Security\Firewall\JiraListener dg_jira_auth.provider.class: DG\JiraAuthBundle\Security\Authentication\Provider\JiraProvider dg_jira_auth.handler.class: DG\JiraAuthBundle\Security\Authentication\Handler\JiraAuthenticationHandler dg_jira.rest.class: DG\JiraAuthBundle\Jira\JiraRest services: dg_jira.rest: class: %dg_jira.rest.class% arguments: - '%jira_url%' dg_jira_auth.user_provider: class: %dg_jira_auth.user_provider.class% arguments: - @dg_jira.rest dg_jira_auth.authentication_success_handler: class: %dg_jira_auth.handler.class% dg_jira_auth.authentication_failure_handler: class: %dg_jira_auth.handler.class% dg_jira_auth.authentication_provider: class: %dg_jira_auth.provider.class% arguments: [@dg_jira_auth.user_provider, '', @dg_jira.rest] dg_jira_auth.authentication_listener: class: %dg_jira_auth.listener.class% arguments: - @security.context - @security.authentication.manager - @security.authentication.session_strategy - @security.http_utils - '' - @dg_jira_auth.authentication_success_handler - @dg_jira_auth.authentication_failure_handler - '' - @logger - @event_dispatcher



Регистрация нового метода аутентификации и авторизации в Symfony

Чтобы все вышеперечисленное работало, необходимо описать поведение аутентификации в виде фабрики и зарегистрировать ее в бандле.



<Эphp namespace DG\JiraAuthBundle\DependencyInjection\Security\Factory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\DefinitionDecorator; use Symfony\Component\DependencyInjection\Reference; class JiraFactory extends AbstractFactory { public function __construct(){ $this->addOption('username_parameter', '_username'); $this->addOption('password_parameter', '_password'); $this->addOption('intention', 'authenticate'); $this->addOption('post_only', true); } protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId) { $provider = 'dg_jira_auth.authentication_provider.'.

$id; $container ->setDefinition($provider, new DefinitionDecorator('dg_jira_auth.authentication_provider')) ->replaceArgument(1, $id) ; return $provider; } protected function getListenerId() { return 'dg_jira_auth.authentication_listener'; } public function getPosition() { return 'form'; } public function getKey() { return 'jira-form'; } protected function createListener($container, $id, $config, $userProvider) { $listenerId = parent::createListener($container, $id, $config, $userProvider); if (isset($config['csrf_provider'])) { $container ->getDefinition($listenerId) ->addArgument(new Reference($config['csrf_provider'])) ; } return $listenerId; } protected function createEntryPoint($container, $id, $config, $defaultEntryPoint) { $entryPointId = 'security.authentication.form_entry_point.'.

$id; $container ->setDefinition($entryPointId, new DefinitionDecorator('security.authentication.form_entry_point')) ->addArgument(new Reference('security.http_utils')) ->addArgument($config['login_path']) ->addArgument($config['use_forward']) ; return $entryPointId; } }

Чтобы зарегистрироваться в бандле, вам нужно добавить строку в метод сборки класса бандла

$extension->addSecurityListenerFactory(new JiraFactory());



Окончательная реализация

Все, теперь мы готовы протестировать работу с Jira. Добавьте созданный JiraUserProvider в файл security.yml в разделе поставщиков в виде строк.



jira_auth_provider: id: dg_jira_auth.user_provider

Далее нужно добавить новый раздел в фаерволы, предполагая, что все страницы, адреса которых начинаются с /jira/, по умолчанию закрыты от неавторизованных пользователей:

jira_secured: provider: jira_auth_provider switch_user: false context: user pattern: /jira/.

* jira_form: check_path: dg_jira_auth_check_path login_path: dg_jira_auth_login_path default_target_path: dg_jira_auth_private logout: path: dg_jira_auth_logout target: dg_jira_auth_public anonymous: true

Последний штрих — добавление в раздел access_controls строк, определяющих роли пользователей, необходимые для просмотра страниц.

Приблизительная форма строк может выглядеть так:

- { path: ^/jira/public, role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/jira/private/login$, role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/jira/private(.

*)$, role: ROLE_USER }



ПС

Весь код, приведенный в статье, можно установить в виде бандла из пакета «dg/jira-auth-bundle» в композиторе или из github .

Для работы бандла необходимо зарегистрировать его в AppKernel.php и добавить раздел

_jira_auth: resource: "@DGJiraAuthBundle/Resources/config/routing.yml" prefix: /jira/

в маршрутизации.

yml. После этого вы можете перейти на страницу /jira/public и протестировать авторизацию через Jira.

Чтобы закрепить материал

В Symfony Cookbook есть то же самое.

инструкции , как реализовать аутентификацию через сторонний веб-сервис.

Надеюсь, статья будет вам полезна! Теги: #symfony2 #php #symfony

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.