Привет, Хабросообщество.
В этой статье я хочу рассказать вам, как можно подружить знаменитый фреймворк Symfony2 и не менее известный трекер Jira.
Зачем связывать Jira и Symfony2?
В компании, где я работаю, возникла необходимость подключить систему поддержки и трекер задач через API, чтобы запросы от клиентов можно было легко конвертировать в тикеты.Основной проблемой, которая встала на нашем пути, была интеграция аутентификации Jira (использовался механизм «Базовая аутентификация») и системы безопасности Symfony2. Чтобы понять механизмы аутентификации и авторизации фреймворка, необходимо ознакомиться с официальной документацией: http://symfony.com/doc/current/book/security.html .
Что нужно для создания нового типа авторизации в Symfony2?
- Токен, который будет хранить вводимые пользователем данные во время аутентификации.
- Прослушиватель, необходимый для проверки аутентификации пользователя.
- Поставщик, который напрямую реализует аутентификацию через Jira.
- Поставщик пользователя, который будет запрошен Symfony2 Security для получения информации о пользователе.
- 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
-
Еженедельный Геймдев: #24 — 27 Июня 2021 Г.
19 Oct, 24 -
Codeforces: Неизвестный Язык, Раунд №3
19 Oct, 24 -
Как Представить Свою Компанию
19 Oct, 24 -
Восстановление Plexconnect Для Apple Tv
19 Oct, 24