Цель данной статьи — рассказать об организации нестандартной аутентификации для проектов на базе Symfony 2.0.
Это может понадобиться, в случае, когда затруднительно организовать доступ к учетным записям пользователей через ORM, либо авторизация осуществляется с помощью внешних ресурсов, например соц.сетей.
Для меня задача выглядела так: у меня был проект на Symfony 1.4 и, с выходом второй версии, “руки чесались” перевести его на 2.0, при этом не меняя фундамента системы, т.е. базу данных и принцип доступа к ней.
Данные в моем проекте хранятся в MongoDB, а доступ осуществлялся через собственную простейшую обертку, умевшую привязывать к нужным коллекциям классы. Привязать обертку к Symfony 2 не составило большого труда.
Задача в части авторизации пользователей, изначально сводилась к получению данных из MongoDB. Задача на улучшение — реализовать возможность авторизации по учетным записям социальных сетей.
Прежде чем приниматься за работу над собственными расширениями системы безопасности для Symfony 2.0 нужно понимать, как работает фреймворк в общем, и компонент Security в частности.
Думаю, что все, кто интересовался второй версией фреймворка в курсе, что входящие запросы рассматриваются, как события или event’ы, и последовательно поступают на обработку listener’ам. В ходе этой обработки и появляется ответ (он же response), возвращаемый клиенту.
В общих чертах все происходит так:
Нас интересуют этап 2 и, отчасти, 3.
Как работает система безопасности — отдельная песня. Процесс выглядит примерно так:
В сравнении с 1.х, где все описанные процессы происходили непосредственно в Action’ах, схема кажется перегруженной, излишне сложной. Однако такая структура дает нам возможность не делать всю работу по организации безопасности в нашем приложении, позволяя изменить только необходимую часть, а остальную работу доверить фреймворку.
Что бы вы ни хотели получить от системы безопасности Symfony 2.0, прежде чем приступить к делу нужно иузить соответствующую документацию. Не обойтись без глав Книги Security и Service Container. Если хочется читать на родном языке, можно изучить эту статью.
На самом деле, если вы не хотите получать информацию о пользователях из “странных” источников, то дальше читать и не обязательно. Работа с базой через Doctrine прекрасно описана в перечисленных статьях.
Symfony 2.0 “из коробки” располагает довольно большим набором набором инструментов авторизации. Есть, как простейший вариант с авторизацией по паре логин/пароль, так и более экзотичные варианты, например авторизация по сертификатам.
Для начала мне нужно организовать самую обыкновенную авторизацию по логину и паролю, но брать данные из собственной базы на MongoDB и через собственную же обертку.
На самом деле это — самая легкая часть задачи. Для начала нам понадобится класс, реализующий интерфейс UserInterface.
Класс очень простой, в данном случае весь функционал по доступу к данным документа из MongoDB выведен в родительский класс, рассмотрение которого не является темой данной статьи. Реализация же интерфейса UserInterface обеспечивает системе безопасности фреймворка возможность получать некоторые данные необходимые данные пользователя и сравнивать объекты пользователей между собой.
Стоит обратить внимание на метод getRoles, в приведенной выше статье данный метод возвращает сущности Role, у нас же на выходе массив с простыми строками, что приемлемо для системы. Если рассматривать роли, как группы, которые могут содержать переменный набор прав, то понадобится возвращать объекты, совместимые с RoleInterface, в нашем случае роли и есть набор прав, по тому нам достаточно строковых значений.
Далее необходим класс, реализующий интерфейс UserProviderInterface, позволяющий получать объекты описанного выше класса.
Теперь необходимо зарегистрировать провайдер как сервис и “научить” компонент безопасности им пользоваться.
В services.yml нашего бандла добавим данные о сервисе и параметрах для него:
В security.yml приложения пропишем:
Есть одна особенность, о которой нигде прямо не упоминается(или я не обратил внимания). Если в security.yml объявлено больше одного провайдера пользователей, то для аутентификации будет использоваться первый, если не другой провайдер указан прямо.
Собственно после этого мы уже можем начинать логиниться, используя данные из MongoDB. В моем случае в базе уже есть данные от предыдущей версии проекта. Как сделать точку входа, описано в приведенных выше статьях, думаю, нет смысла писать об этом еще раз.
В качестве нововведения в новой версии проекта я решил добавить возможность авторизовать пользователей по из учеткам в соц. сетях. В качестве примера расскажу о аутентификации и регистрации через всеми горячо “любимую” соц. сеть “ВКонтакте”.
Создание собственного метода аутентификации в Symfiny 2.0 можно считать документированным. Существует официальная статья, расказывающая о реализации аутентификации с помощью WSSE. Начиная работу на нужным мне функционалом, я ориентировался на эту статью. Принципы работы с соц. сетью я почерпнул из этой статьи на хабре.
Теперь немного анализа полученной информации. Аутентификация, описанная для примера в официальной статье не слишком похожа на то, что мне требуется. Процесс аутентифицировать пользователя по учетной записи ВКонтакта скорее схож с обычной(на базе логина/пароля) аутентификацией, однако при этом ни логина, ни пароля у нас нет. Поразмыслив над этим я решил изучить исходники Symfony, в часности то, как устроена обчная авторизация через форму(form_login).
Выяснилось, что встроенные AuthenticationListener’ы и AuthenticationProvider’ы наследуют от абстрактных классов, берущих на себя большую часть рутинной работы, связанной с непосредственным управлением аутентификацией в Symfony, работой с сессией пользователя и пр. Мы так же можем использовать эти классы для облегчения своей работы.
Приступим.
Как и в официальной статье, начнем с Token’а:
Класс token’а, как и в случае с классом пользователя очень прост. Далее нам потребуется Listener, который займется созданием Token’ов только что описанного класса.
По сути роль listener’а сводится к извлечению из запроса данных, необходимых для аутентификации пользователя. AbstractAuthenticationListener, предоставляет нам возможность ограничится написанием исключительно кода проверки и разбора запроса, предоставляя готовый функционал фильтрации запросов по URI, управления перенаправлениями и др. рутины, так же входящей в “обязанности” AuthenticationListener’а.
После реализации Listenr’a нам потребуется AuthenticationProvider, который будет осуществлять непосредственную проверку Token’а и “объявлять” об успешной аутентификации.
После завершения работы над AuthenticationProvider’ом остается только научить фреймворк пользоваться написанными нами классами. В отличии от UserProvider’а, это довольно объемная и не “прозрачная” часть работы.
Официальная документация сообщает, что для использования нашего кода системой безопасности Symfony потребуется написать реализацию SecurityFactoryInterface. Так и поступим.
После создания Factory остается только дополнить конфигурацию.
Начнем с security.yml. Туда необходимо добасить ссылку на файл конфигурации нешей Factory...
… далее, создадим и заполним файл, на который ссылаемся, синтаксис внем аналогичен синтаксису обычного services.yml.
Теперь необходимо дополнить сам services.yml сервисами, на которые мы ссылаемся в нашем Factory:
Последнее, что остается сделать, это включит нашу подсистему в конфигурацию приложения. Для этого снова правим security.yml, а в нем, блок firewalls:
На этом все. Добавив в шаблон формы входа ссылку на авторизацию через ВКонтакте мы сможем авторизоваться с помощью этой соц. сети.
Система безопасности Symfony 2.0 с начала может казаться сложной и запутанной. В общем-то она действительно сложная и запутанная, но если сравнить организацию безопасности во второй и первой версиях, то можно увидеть одну тенденцию.
Система безопасности организована таким образом, чтобы типовые задачи могли решаться на уровне конфигурирования фреймфорка. Нам не нужно программировать для проверки логина/пароля (кроме рисования формы), нам даже action для проверки делать не нужно. То же для logout’а. Сложность реализации отдельных элементов связана в первую очередь с необходимостью встраивать их в гибкую архитектуру безопасности.
Спасибо за внимание.
Зачем это может понадобится?
Это может понадобиться, в случае, когда затруднительно организовать доступ к учетным записям пользователей через ORM, либо авторизация осуществляется с помощью внешних ресурсов, например соц.сетей.
Задача
Для меня задача выглядела так: у меня был проект на Symfony 1.4 и, с выходом второй версии, “руки чесались” перевести его на 2.0, при этом не меняя фундамента системы, т.е. базу данных и принцип доступа к ней.
Данные в моем проекте хранятся в MongoDB, а доступ осуществлялся через собственную простейшую обертку, умевшую привязывать к нужным коллекциям классы. Привязать обертку к Symfony 2 не составило большого труда.
Задача в части авторизации пользователей, изначально сводилась к получению данных из MongoDB. Задача на улучшение — реализовать возможность авторизации по учетным записям социальных сетей.
Немного теории
Прежде чем приниматься за работу над собственными расширениями системы безопасности для Symfony 2.0 нужно понимать, как работает фреймворк в общем, и компонент Security в частности.
Думаю, что все, кто интересовался второй версией фреймворка в курсе, что входящие запросы рассматриваются, как события или event’ы, и последовательно поступают на обработку listener’ам. В ходе этой обработки и появляется ответ (он же response), возвращаемый клиенту.
В общих чертах все происходит так:
- ядро получает запрос;
- запрос обрабатывает система безопасности;
- запрос обрабатывает “маршрутизатор”, определяется запрошенный контроллер;
- выполняется Action и сопутствующие процессы подготовки ответа;
- сформированный ответ проходит свою часть пути через обработчики, где вносятся последние корректировки.
Нас интересуют этап 2 и, отчасти, 3.
Как работает система безопасности — отдельная песня. Процесс выглядит примерно так:
- Общий listener системы безопасности получает событие-запрос и раздает его “дочерним” listener’ам аутентификации.
- listener’ы формируют token и пытаются его авторизовать с помощью AuthenticationManager’а;
- AuthenticationManager подбирает подходящего провайдера аутентификации и отдает token ему на проверку.
- провайдер аутентификации возвращает менеджеру, а то, в свою очередь listener’y авторизованный(или не авторизванный) token. На данном этапе задействуется UserProvider, который и работает с данными о пользователях.
- listener, в зависимости от результата, возвращенного AuthenticationManager’ом и собственного кода, либо “подсовывает” авторизованный token SecurityContex’у, либо формирует ответ(Response), либо делает и то, и другое. Если listener формирует ответ, то событие-запрос не поступит на обработку “маршрутизатору” и не будет обращения к контролеру.
- далее происходит авторизация, этот и последующий этапы я не разбирал, но принцип в целом схож с аутентификацией, как я понял.
В сравнении с 1.х, где все описанные процессы происходили непосредственно в Action’ах, схема кажется перегруженной, излишне сложной. Однако такая структура дает нам возможность не делать всю работу по организации безопасности в нашем приложении, позволяя изменить только необходимую часть, а остальную работу доверить фреймворку.
Шаг 0 — изучаем документацию.
Что бы вы ни хотели получить от системы безопасности Symfony 2.0, прежде чем приступить к делу нужно иузить соответствующую документацию. Не обойтись без глав Книги Security и Service Container. Если хочется читать на родном языке, можно изучить эту статью.
На самом деле, если вы не хотите получать информацию о пользователях из “странных” источников, то дальше читать и не обязательно. Работа с базой через Doctrine прекрасно описана в перечисленных статьях.
Шаг 1 — своя база пользователей.
Symfony 2.0 “из коробки” располагает довольно большим набором набором инструментов авторизации. Есть, как простейший вариант с авторизацией по паре логин/пароль, так и более экзотичные варианты, например авторизация по сертификатам.
Для начала мне нужно организовать самую обыкновенную авторизацию по логину и паролю, но брать данные из собственной базы на MongoDB и через собственную же обертку.
На самом деле это — самая легкая часть задачи. Для начала нам понадобится класс, реализующий интерфейс UserInterface.
namespace MyBundle\Models;
use Mh\Mongo\Model\Base;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class User extends Base implements UserInterface {
public function getRoles() {
return $this->credentials;
}
public function getPassword() {
return $this->passw;
}
public function getSalt() {
return $this->salt;
}
public function getUsername() {
return $this->uname;
}
public function eraseCredentials() {
}
public function equals(UserInterface $user) {
return $this->getUsername() === $user->getUsername();
}
public function __toString() {
return $this->uname;
}
}
Класс очень простой, в данном случае весь функционал по доступу к данным документа из MongoDB выведен в родительский класс, рассмотрение которого не является темой данной статьи. Реализация же интерфейса UserInterface обеспечивает системе безопасности фреймворка возможность получать некоторые данные необходимые данные пользователя и сравнивать объекты пользователей между собой.
Стоит обратить внимание на метод getRoles, в приведенной выше статье данный метод возвращает сущности Role, у нас же на выходе массив с простыми строками, что приемлемо для системы. Если рассматривать роли, как группы, которые могут содержать переменный набор прав, то понадобится возвращать объекты, совместимые с RoleInterface, в нашем случае роли и есть набор прав, по тому нам достаточно строковых значений.
Далее необходим класс, реализующий интерфейс UserProviderInterface, позволяющий получать объекты описанного выше класса.
namespace MyBundle\Security;
use Mh\Mongo\MongoBundle\ConnectionManager;
use MyBundle\Models\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use \Exception;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
class UserProvider implements UserProviderInterface {
protected $collection;
protected $db;
protected $field;
protected $logger;
// Конструктор класса, нужно понимать, что получать мы будем экземпляры
// как сервисы, поэтому все нужное надо передать в конструктор в качестве
// аргументов.
public function __construct(array $params, ConnectionManager $cm, LoggerInterface $logger) {
// из параметров получаем данные для коннекта к БД
$this->db = $cm->getConnection($params['confname']);
$this->collection = $this->db->selectCollection($params['collection']);
// так же в настройки вынесено название поля, содержащего логин
$this->field = $params['field'];
if (!$this->field || !$this->collection) {
throw new Exception("Invalid parameters");
}
// так же получаем экземпляр logger’а, все таки DI
$this->logger = $logger;
}
// Данный метод необходим для получения пользователя по логину,
// используется при авторизации пользователя.
public function loadUserByUsername($uname) {
// для наглядности будем логировать все действия.
$this->logger->debug("user load request. name: $uname");
// здесь мы с помощью обертки получаем экземпляр класса User
// или пустой массив.
$user = $this->collection->findOne(array($this->field => $uname));
// выбрасываем спец. исключение, если пользователь не найден.
if (!isset($user->uname) || $user->uname !== $uname) {
throw new UsernameNotFoundException(sprintf('User "%s" does not exist.', $uname));
}
// и возвращаем найденного пользователя
return $user;
}
// Метод используется при новом запросе авторизованного пользователя.
// Обновляет иноформацию о пользователе в сессии из БД.
public function refreshUser(UserInterface $user) {
if (!($user instanceof User))
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
$this->logger->info("refresh from mongo");
$_user = $this->collection->findOne(array('_id' => $user->_id));
if ($_user && $_user instanceof User)
$this->logger->info("roles: " .implode(', ',$_user->roles));
else
throw new UsernameNotFoundException(sprintf('User "%s" does not exist.', $user->uname));
return $_user;
}
// Метод проверки класса пользователя.
public function supportsClass($class) {
$this->logger->debug("support checking: $class");
if ($class == 'MyBundle\Models\User')
return true;
return false;
}
}
Теперь необходимо зарегистрировать провайдер как сервис и “научить” компонент безопасности им пользоваться.
В services.yml нашего бандла добавим данные о сервисе и параметрах для него:
parameters: my.users: confname: default collection: user field: uname services: my.users.prov: class: MyBundle\Security\UserProvider arguments: [%my.users%, @mongo.manager, @monolog.logger]
В security.yml приложения пропишем:
security: encoders: Symfony\Component\Security\Core\User\User: plaintext # для простоты запуска можно хранить в базе нешифрованный пароль MyBundle\Models\User: plaintext providers: # теперь объявим новый источник пользователей (имя можно назначить любое) # и пропишем в нем имя сервиса, который будет отдавать данные # о пользователях mongobase: id: my.userprov ...
Есть одна особенность, о которой нигде прямо не упоминается(или я не обратил внимания). Если в security.yml объявлено больше одного провайдера пользователей, то для аутентификации будет использоваться первый, если не другой провайдер указан прямо.
Собственно после этого мы уже можем начинать логиниться, используя данные из MongoDB. В моем случае в базе уже есть данные от предыдущей версии проекта. Как сделать точку входа, описано в приведенных выше статьях, думаю, нет смысла писать об этом еще раз.
Шаг 2 — OAuth аутентификация
В качестве нововведения в новой версии проекта я решил добавить возможность авторизовать пользователей по из учеткам в соц. сетях. В качестве примера расскажу о аутентификации и регистрации через всеми горячо “любимую” соц. сеть “ВКонтакте”.
Создание собственного метода аутентификации в Symfiny 2.0 можно считать документированным. Существует официальная статья, расказывающая о реализации аутентификации с помощью WSSE. Начиная работу на нужным мне функционалом, я ориентировался на эту статью. Принципы работы с соц. сетью я почерпнул из этой статьи на хабре.
Теперь немного анализа полученной информации. Аутентификация, описанная для примера в официальной статье не слишком похожа на то, что мне требуется. Процесс аутентифицировать пользователя по учетной записи ВКонтакта скорее схож с обычной(на базе логина/пароля) аутентификацией, однако при этом ни логина, ни пароля у нас нет. Поразмыслив над этим я решил изучить исходники Symfony, в часности то, как устроена обчная авторизация через форму(form_login).
Выяснилось, что встроенные AuthenticationListener’ы и AuthenticationProvider’ы наследуют от абстрактных классов, берущих на себя большую часть рутинной работы, связанной с непосредственным управлением аутентификацией в Symfony, работой с сессией пользователя и пр. Мы так же можем использовать эти классы для облегчения своей работы.
Приступим.
Как и в официальной статье, начнем с Token’а:
namespace MyBundle\Social\Authentication;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
class Token extends AbstractToken {
// сохраняем нужные нам для проверки параметры.
public $social;
public $hash;
public $add;
// прописываем стандартный конструктор, обращающийся к родителю
function __construct(array $roles = array()) {
parent::__construct($roles);
// по примеру встроенной аутентификации, добавляем в конструктор
// прямое указание на аутентификацию токена с не пустым списком ролей.
parent::setAuthenticated(count($roles) > 0);
}
// метод, необходимый для реализации TokenInterface
public function getCredentials() {
}
// поскольку токены проверяются при обработке каждом новом запросе клиента,
// нам необходимо сохранять нужные нам данные. В связи с этим “обертываем”
// унаследованные методы сериализации и десериализации.
public function serialize() {
$pser = parent::serialize();
return serialize(array($this->social, $this->hash, $this->add, $pser));
}
public function unserialize($serialized) {
list($this->social, $this->hash, $this->add, $pser) = unserialize($serialized);
parent::unserialize($pser);
}
}
Класс token’а, как и в случае с классом пользователя очень прост. Далее нам потребуется Listener, который займется созданием Token’ов только что описанного класса.
namespace MyBundle\Social\Authentication;
// по скольку мы наследуем от встроеенного AbstractAuthenticationListener,
// за удобство работы придется “платить” многочисленными аргументами конструктора,
// каждый из которых требукт проверки класса
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
class AuthenticationListener extends AbstractAuthenticationListener {
// для работы нам потребуются параметры “общения” с соц. сетями
protected $social;
// переопределяем конструктор абстрактного предка, добавляем
// дополнительный аргумент
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, array $options = array(), AuthenticationSuccessHandlerInterface $successHandler = null, AuthenticationFailureHandlerInterface $failureHandler = null, LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, array $social = array()) {
parent::__construct($securityContext, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, array_merge(array(
'intention' => 'authenticate',
), $options), $successHandler, $failureHandler, $logger, $dispatcher);
$this->social = $social;
}
// метод attemptAuthentication производит всю работу, связанную с
// извлечением из запроса информации, необходимой для создания Token’а
public function attemptAuthentication(Request $request) {
// проверяем запрос на наличие необходимых полей, а так же проверяем
// наличие куки от ВКонтакта
if ($request->get('uid') && $request->get('hash') && $request->cookies->get("vk_app_{$this->social['vk']['id']}")) {
$this->logger->debug("vk auth handled");
// извлекаем информацию из запроса
$uid = $request->get('uid');
$fn = $request->get('first_name');
$ln = $request->get('last_name');
$hash = $request->get('hash');
$this->logger->info("user $fn $ln [$uid] // $hash");
$avatars = array(
'sav' => $request->get('photo'),
'srav' => $request->get('photo_rec'),
);
// создаем новый token и прописываем в него
// псевдологин пользователя соц. сети …
$token = new Token();
$token->setUser("vk{$uid}");
// … далее заполняем специфические поля token’а ...
$token->social = 'vk';
$token->hash = $hash;
$token->add = array(
'uid' => $uid,
'avatar' => $avatars,
'name' => "$fn $ln",
);
// … и передаем его на проверку, возвращая результат этой проверки
return $this->authenticationManager->authenticate($token);
}
// если запрос не содержит данных для авторизации - не возвращаем ничего.
}
}
По сути роль listener’а сводится к извлечению из запроса данных, необходимых для аутентификации пользователя. AbstractAuthenticationListener, предоставляет нам возможность ограничится написанием исключительно кода проверки и разбора запроса, предоставляя готовый функционал фильтрации запросов по URI, управления перенаправлениями и др. рутины, так же входящей в “обязанности” AuthenticationListener’а.
После реализации Listenr’a нам потребуется AuthenticationProvider, который будет осуществлять непосредственную проверку Token’а и “объявлять” об успешной аутентификации.
namespace MyBundle\Social\Authentication;
use Mh\Mongo\MongoBundle\ConnectionManager;
use MyBundle\Models\User as User;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\NonceExpiredException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
// В случае с AuthenticationProvider’ом я решил ограничится реализацией
// интерфейса, не наследуя от абстрактного UserAuthenticationProvider,
// по скольку функция данного класса более проста и требуется от него меньше.
class Provider implements AuthenticationProviderInterface {
protected $userProvider;
protected $logger;
// нам вновь потребуются параметры соц. сетей
protected $social;
// а так же сервис менеджера коннектов MongoDB для
// регистрации новых пользователей
protected $mongo;
// прописываем конструктор класса, принимающий все необходимые для
// работы параметры.
public function __construct(UserProviderInterface $userProvider, array $social, ConnectionManager $cm, LoggerInterface $logger) {
$this->userProvider = $userProvider;
$this->social = $social;
$this->mongo = $cm;
$this->logger = $logger;
}
// основной метод класса
public function authenticate(TokenInterface $token) {
$user = null;
// пытаемся найти пользователя по средствам UserProvider’a
// для решения нашей задачи не критично, если пользователь не будет найден
try {
$user = $this->userProvider->loadUserByUsername($token->getUsername());
} catch (UsernameNotFoundException $ex) {
$this->logger->debug("user ".$token->getUsername()." not yet registred");
}
try {
// начинаем с проверки hash’а
if ($this->checkHash($token)) {
$this->logger->info("hash is valid");
// в случае, если передан корректный hash, а пользователь
// отсутствует в базе - выполняем “регистрацию”
if (!$user) {
$this->logger->info("register new user");
$user = new User(array(
'uname' => $token->getUsername(),
'social' => $token->social,
'fullname' => $token->add['name'],
'avatar' => $token->add['avatar'],
'suid' => $token->add['uid'],
'roles' => array('ROLE_EXTUSER', strtoupper($token->social) ),
));
$user->save($this->mongo);
}
// следуя примеру из оф. документации, создаем новый Token
// и наполняем его необходимой информацией
$authenticatedToken = new Token($user->getRoles());
$authenticatedToken->social = $token->social;
$authenticatedToken->hash = $token->hash;
$authenticatedToken->add = $token->add;
$authenticatedToken->setUser($user);
// и возвращаем в качестве результата работы метода
return $authenticatedToken;
} else {
$this->logger->debug("hash is invalid.");
}
} catch (\Exception $ex) {
$this->logger->err("auth internal exception: $ex");
}
// если по каким-то причинам аутентификация не состоялась - запускаем
// специальное исключение.
throw new AuthenticationException('The Social authentication failed.');
}
// метод проверки hesh’ей, осуществляющий специфические действия для каждой
// подключенной службы.
protected function checkHash(Token $token) {
if ($token->social == 'vk') {
return ($token->hash === md5( $this->social['vk']['id'] . $token->add['uid'] . $this->social['vk']['key'] ));
}
return false;
}
// реализуем интерфейс, метод проверки совместимости token’а и provider’а
public function supports(TokenInterface $token) {
return $token instanceof Token;
}
}
После завершения работы над AuthenticationProvider’ом остается только научить фреймворк пользоваться написанными нами классами. В отличии от UserProvider’а, это довольно объемная и не “прозрачная” часть работы.
Официальная документация сообщает, что для использования нашего кода системой безопасности Symfony потребуется написать реализацию SecurityFactoryInterface. Так и поступим.
namespace MyBundle\DependencyInjection\Security;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
// В коде Symfony есть класс AbstractFactory который я почти полностью копирую.
// Это связано с тем, что струкутра наследника показалась мне перегруженой, как и
// в случае с AuthenticationProvider’ом
class SocialAuthFactory implements SecurityFactoryInterface {
// прописываем набор базовых опций будущего firewall’а и их значения
// по умолчанию. Набор опций совпадает с form_login.
protected $options = array(
'check_path' => '/login_check',
'login_path' => '/login',
'use_forward' => false,
'always_use_default_target_path' => false,
'default_target_path' => '/',
'target_path_parameter' => '_target_path',
'use_referer' => false,
'failure_path' => null,
'failure_forward' => false,
);
// основной метод, в котором мы связываем в единый firewall наши listener и
// provider.
// в качестве аргументов метод принимает контейнер, id firewall’а из
// конфигурации, а так же прописанные в конфигурации параметры.
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) {
// К сожалению в документации не разъясняется, что именно и как
// делает этот метод.
// В общих чертах здесь производит “обертывание” наших сервисов
// (о них ниже) с частичной заменой аргументов.
// Объявлется id нового сервиса для AuthenticationProvider’а
$providerId = 'security.authentication.provider.social.'.$id;
// Затем создается сервис - декоратор для нашего my.socialauth.prov
// и первый(0й) аргумент заменяется на ссылку на один из аргументов
// данного метода
$container
->setDefinition($providerId, new DefinitionDecorator('my.socialauth.prov'))
->replaceArgument(0, new Reference($userProvider))
;
// для listener’а совершаются те же действия, что и для provider’а
$listenerId = 'security.authentication.listener.social.'.$id;
$container
->setDefinition($listenerId, new DefinitionDecorator('my.socialauth.listener'))
->replaceArgument(4, $id)
->replaceArgument(5, array_intersect_key($config, $this->options));
// метод возвращает id новых сервисов.
return array($providerId, $listenerId, $defaultEntryPoint);
}
// метод сообщает, на каком этапе обработки запроса следует
// использовать наши классы. Снова делаем по аналогии с login_form.
public function getPosition() {
return 'form';
}
// метод сообщает имя созданной нами подсистемы, это имя в конфигурации даст
// фреймворку указание использовать наш код.
public function getKey() {
return 'mysocial';
}
// метод создание конфигурации, практически полностью копирует аналогичный в
// AbstractFactory
public function addConfiguration(NodeDefinition $node) {
$builder = $node->children();
$builder
->scalarNode('provider')->end()
;
foreach ($this->options as $name => $default) {
if (is_bool($default)) {
$builder->booleanNode($name)->defaultValue($default);
} else {
$builder->scalarNode($name)->defaultValue($default);
}
}
}
}
После создания Factory остается только дополнить конфигурацию.
Начнем с security.yml. Туда необходимо добасить ссылку на файл конфигурации нешей Factory...
security: factories: - "%kernel.root_dir%/../src/MyBundle/Resources/config/socialauth_factory.yml" ...
… далее, создадим и заполним файл, на который ссылаемся, синтаксис внем аналогичен синтаксису обычного services.yml.
services: security.authentication.factory.mysocial: class: MyBundle\DependencyInjection\Security\Factory\SocialFactory tags: - { name: security.listener.factory }
Теперь необходимо дополнить сам services.yml сервисами, на которые мы ссылаемся в нашем Factory:
parameters: my.users: confname: default collection: user field: uname my.social: id: __YOUR_APP_ID__ key: __YOUR_APP_PRIVATE_KEY__ services: my.users.prov: class: MyBundle\Security\UserProvider arguments: [%my.users%, @mongo.manager, @monolog.logger] # сервис для listener’а мы объявляем как наследник сервиса AbstractListener’а # указанные здесь аргументы будут переданы конструктору после аргументов, # прописанных в сервисе-родителе my.socialauth.listener: class: MyBundle\Social\Authentication\Listener parent: security.authentication.listener.abstract arguments: [%my.social%] # в сервисе для provider’а мы оставляем первый аргумент без настоящего значения. # Значение будет передано из декоратора, созданного в Factory. my.socialauth.prov: class: MyBundle\Social\Authentication\Provider arguments: ['', %my.social%, @mongo.manager, @monolog.logger]
Последнее, что остается сделать, это включит нашу подсистему в конфигурацию приложения. Для этого снова правим security.yml, а в нем, блок firewalls:
... firewalls: myauth: pattern: ^/ # добавляем нашу firewall в блок к обчному form_login, # напомним, что у него почти такие же настройки. mysocial: check_path: /login/socialauth login_path: /login/in # наши классы будет работать на равных с коробочными классаи Symfony, # чтобы они не мешали друг-другу, сделаем разные check_path form_login: check_path: /login/auth login_path: /login/in # мы позволяем авторизоваться 2-мя способами, выход же для # обоих типов пользователю осуществляется стандартными средствами logout: path: /login/out target: / invalidate_session: true anonymous: ~ ...
На этом все. Добавив в шаблон формы входа ссылку на авторизацию через ВКонтакте мы сможем авторизоваться с помощью этой соц. сети.
Итоги
Система безопасности Symfony 2.0 с начала может казаться сложной и запутанной. В общем-то она действительно сложная и запутанная, но если сравнить организацию безопасности во второй и первой версиях, то можно увидеть одну тенденцию.
Система безопасности организована таким образом, чтобы типовые задачи могли решаться на уровне конфигурирования фреймфорка. Нам не нужно программировать для проверки логина/пароля (кроме рисования формы), нам даже action для проверки делать не нужно. То же для logout’а. Сложность реализации отдельных элементов связана в первую очередь с необходимостью встраивать их в гибкую архитектуру безопасности.
Спасибо за внимание.