- PVSM.RU - https://www.pvsm.ru -
Здравствуйте, Хабросообщество. В этой статье хочу рассказать, как можно подружить известный фреймворк Symfony2 и не менее известный трекер Jira.
В компании, где я работаю, возникла необходимость связать систему саппорта и трекер задач через API, чтобы заявки от клиентов могли быть легко преобразованы в тикеты. Первостепенной проблемой, которая встала на нашем пути, была интеграция аутентификации Jira (использовался механизм “Basic Authentication”) и системы безопасности Symfony2. Для понимания механизмов аутентификации и авторизации фреймворка необходимо ознакомиться с официальной документацией: http://symfony.com/doc/current/book/security.html [1].
Для сохранения информации, введенной при аутентификации пользователями, и последующего ее использования в Symfony используются токены, которые наследуются от класса AbstractToken. В рассматриваемой задаче необходимо хранить 2 поля — это логин и пароль пользователя, на основе которых будет производить проверка авторизованности в Jira. Код реализации класса токена приведен ниже.
<?php
namespace DGJiraAuthBundleSecurityAuthenticationToken;
use SymfonyComponentSecurityCoreAuthenticationTokenAbstractToken;
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 DGJiraAuthBundleSecurityFirewall;
use DGJiraAuthBundleSecurityAuthenticationTokenJiraToken;
use SymfonyComponentEventDispatcherEventDispatcherInterface;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;
use PsrLogLoggerInterface;
use SymfonyComponentSecurityCoreAuthenticationAuthenticationManagerInterface;
use SymfonyComponentSecurityCoreAuthenticationTokenTokenInterface;
use SymfonyComponentSecurityCoreExceptionAuthenticationException;
use SymfonyComponentSecurityCoreSecurityContextInterface;
use SymfonyComponentSecurityHttpFirewallAbstractAuthenticationListener;
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. Для работы с rest api трекера написан простой класс, который подключается в виде сервиса. Для работы с API Jira используется библиотека Buzz.
<?php
namespace DGJiraAuthBundleJira;
use BuzzMessage;
use BuzzClientCurl;
class JiraRest {
private $jiraUrl = '';
public function __construct($jiraUrl){
$this->jiraUrl = $jiraUrl;
}
public function getUserInfo($username, $password){
$request = new MessageRequest(
'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 MessageResponse();
$client = new Curl();
$client->setTimeout(10);
$client->send($request, $response);
return $response;
}
}
Provider должен реализовывать интерфейс AuthenticationProviderInterface и выглядит следующим образом:
<?php
namespace DGJiraAuthBundleSecurityAuthenticationProvider;
use DGJiraAuthBundleEntityUser;
use DGJiraAuthBundleJiraJiraRest;
use DGJiraAuthBundleSecurityAuthenticationTokenJiraToken;
use SymfonyComponentSecurityCoreAuthenticationProviderAuthenticationProviderInterface;
use SymfonyComponentSecurityCoreAuthenticationTokenTokenInterface;
use SymfonyComponentSecurityCoreExceptionAuthenticationException;
use SymfonyComponentSecurityCoreUserUserProviderInterface;
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 DGJiraAuthBundleUser;
use DGJiraAuthBundleEntityUser;
use DGJiraAuthBundleJiraJiraRest;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationSessionSession;
use SymfonyComponentSecurityCoreExceptionUnsupportedUserException;
use SymfonyComponentSecurityCoreExceptionUsernameNotFoundException;
use SymfonyComponentSecurityCoreUserUserInterface;
use SymfonyComponentSecurityCoreUserUserProviderInterface;
use SymfonyComponentSecurityCoreSecurityContextInterface;
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 === 'DGJiraAuthBundleEntityUser';
}
}
Для использования созданных классов необходимо их зарегистрировать в конфигурации в виде сервисов. Пример services.yml приведен ниже. Отмечу, что параметр jira_url должен быть определен в parameters.yml и содержать url адрес до Jira.
parameters:
dg_jira_auth.user_provider.class: DGJiraAuthBundleUserJiraUserProvider
dg_jira_auth.listener.class: DGJiraAuthBundleSecurityFirewallJiraListener
dg_jira_auth.provider.class: DGJiraAuthBundleSecurityAuthenticationProviderJiraProvider
dg_jira_auth.handler.class: DGJiraAuthBundleSecurityAuthenticationHandlerJiraAuthenticationHandler
dg_jira.rest.class: DGJiraAuthBundleJiraJiraRest
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
Чтобы все вышеописанное заработало, необходимо описать поведение аутентификации в виде фабрики и зарегистрировать ее в бандле.
<?php
namespace DGJiraAuthBundleDependencyInjectionSecurityFactory;
use SymfonyBundleSecurityBundleDependencyInjectionSecurityFactoryAbstractFactory;
use SymfonyComponentConfigDefinitionBuilderNodeDefinition;
use SymfonyComponentDependencyInjectionContainerBuilder;
use SymfonyComponentDependencyInjectionDefinitionDecorator;
use SymfonyComponentDependencyInjectionReference;
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 }
Весь код, приведенный в статье, можно установить в виде бандла из пакета «dg/jira-auth-bundle» в composer. Для работы бандла, необходимо зарегистрировать его в AppKernel.php и добавить секцию
_jira_auth:
resource: "@DGJiraAuthBundle/Resources/config/routing.yml"
prefix: /jira/
в routing.yml. После этого можно зайти на страницу /jira/public и протестировать авторизацию через Jira.
В Symfony Cookbook есть так же инструкция [2], как внедрить аутентификацию через сторонний веб сервис.
Надеюсь статья будет вам полезна!
Автор: GrizliK1988
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/51305
Ссылки в тексте:
[1] http://symfony.com/doc/current/book/security.html: http://symfony.com/doc/current/book/security.html
[2] инструкция: http://symfony.com/doc/current/cookbook/security/custom_authentication_provider.html
[3] Источник: http://habrahabr.ru/post/206888/
Нажмите здесь для печати.