Pull to refresh

Нестандартная аутентификация в проектах на Symfony 2.0

Symfony *
Sandbox
Цель данной статьи — рассказать об организации нестандартной аутентификации для проектов на базе Symfony 2.0.

Зачем это может понадобится?


Это может понадобиться, в случае, когда затруднительно организовать доступ к учетным записям пользователей через ORM, либо авторизация осуществляется с помощью внешних ресурсов, например соц.сетей.


Задача


Для меня задача выглядела так: у меня был проект на Symfony 1.4 и, с выходом второй версии, “руки чесались” перевести его на 2.0, при этом не меняя фундамента системы, т.е. базу данных и принцип доступа к ней.

Данные в моем проекте хранятся в MongoDB, а доступ осуществлялся через собственную простейшую обертку, умевшую привязывать к нужным коллекциям классы. Привязать обертку к Symfony 2 не составило большого труда.

Задача в части авторизации пользователей, изначально сводилась к получению данных из MongoDB. Задача на улучшение — реализовать возможность авторизации по учетным записям социальных сетей.

Немного теории


Прежде чем приниматься за работу над собственными расширениями системы безопасности для Symfony 2.0 нужно понимать, как работает фреймворк в общем, и компонент Security в частности.

Думаю, что все, кто интересовался второй версией фреймворка в курсе, что входящие запросы рассматриваются, как события или event’ы, и последовательно поступают на обработку listener’ам. В ходе этой обработки и появляется ответ (он же response), возвращаемый клиенту.

В общих чертах все происходит так:
  1. ядро получает запрос;
  2. запрос обрабатывает система безопасности;
  3. запрос обрабатывает “маршрутизатор”, определяется запрошенный контроллер;
  4. выполняется Action и сопутствующие процессы подготовки ответа;
  5. сформированный ответ проходит свою часть пути через обработчики, где вносятся последние корректировки.

Нас интересуют этап 2 и, отчасти, 3.

Как работает система безопасности — отдельная песня. Процесс выглядит примерно так:
  1. Общий listener системы безопасности получает событие-запрос и раздает его “дочерним” listener’ам аутентификации.
  2. listener’ы формируют token и пытаются его авторизовать с помощью AuthenticationManager’а;
  3. AuthenticationManager подбирает подходящего провайдера аутентификации и отдает token ему на проверку.
  4. провайдер аутентификации возвращает менеджеру, а то, в свою очередь listener’y авторизованный(или не авторизванный) token. На данном этапе задействуется UserProvider, который и работает с данными о пользователях.
  5. listener, в зависимости от результата, возвращенного AuthenticationManager’ом и собственного кода, либо “подсовывает” авторизованный token SecurityContex’у, либо формирует ответ(Response), либо делает и то, и другое. Если listener формирует ответ, то событие-запрос не поступит на обработку “маршрутизатору” и не будет обращения к контролеру.
  6. далее происходит авторизация, этот и последующий этапы я не разбирал, но принцип в целом схож с аутентификацией, как я понял.

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

Спасибо за внимание.
Tags:
Hubs:
Total votes 25: ↑25 and ↓0 +25
Views 17K
Comments 6
Comments Comments 6