Цель данной статьи — рассказать об организации нестандартной аутентификации для проектов на базе 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’а. Сложность реализации отдельных элементов связана в первую очередь с необходимостью встраивать их в гибкую архитектуру безопасности.
Спасибо за внимание.
