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

от автора

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

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

В компании, где я работаю, возникла необходимость связать систему саппорта и трекер задач через API, чтобы заявки от клиентов могли быть легко преобразованы в тикеты. Первостепенной проблемой, которая встала на нашем пути, была интеграция аутентификации Jira (использовался механизм “Basic Authentication”) и системы безопасности Symfony2. Для понимания механизмов аутентификации и авторизации фреймворка необходимо ознакомиться с официальной документацией: http://symfony.com/doc/current/book/security.html.

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

  1. Token, который будет хранить введенную пользователем информацию при аутентификации.
  2. Listener, необходимый для проверки авторизованности пользователя.
  3. Provider, непосредственно реализующий аутентификацию через Jira.
  4. User Provider, который будет запрашиваться Symfony2 Security для получения информации о пользователе.
  5. Factory, которая зарегистрирует новый способ аутентификации и авторизации.

Создаем Token

Для сохранения информации, введенной при аутентификации пользователями, и последующего ее использования в 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

Теперь, когда у нас хранятся пользовательские данные, должна быть возможность их проверки на корректность. В случае, если данные устарели, необходимо сообщить об этом фреймворку. Для этого необходимо реализовать 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. Provider

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

<?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;     } } 

Provider должен реализовывать интерфейс 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, чтобы подстраховать себя от временной недоступности трекера. Подобная “страховка” выходит за рамки статьи, но может быть весьма полезна.

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

Система Security во фреймворке запрашивает информацию о пользователе для проверки авторизации. Понятно, что подобная информация находится в 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 должен быть определен в parameters.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;     } } 

Для регистрации в бандле, необходимо в метод build у класса бандла добавить строку

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

Окончательное внедрение

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

        jira_auth_provider:             id: dg_jira_auth.user_provider 

Далее необходимо добавить в firewalls новую секцию, полагая, что все страницы, адреса которых начинаются с /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 } 

PS

Весь код, приведенный в статье, можно установить в виде бандла из пакета «dg/jira-auth-bundle» в composer. Для работы бандла, необходимо зарегистрировать его в AppKernel.php и добавить секцию

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

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

Для закрепления материала

В Symfony Cookbook есть так же инструкция, как внедрить аутентификацию через сторонний веб сервис.

Надеюсь статья будет вам полезна!

ссылка на оригинал статьи http://habrahabr.ru/post/206888/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *