MYSTERRIA3.0

Symfony2 эксклюзивные сессии в симфони

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

Первое, что меня категорически не устроило в поведении симфони по умолчанию - это методы работы с сессиями. Во-первых мне в моем проекте нужны исключительно "эксклюзивные" сессии, то есть для одного пользователя поднимается только одна сессия только на одно устройство. Такая потребность продиктована политикой ведения интерактива с пользователем и служит тому, чтобы эвенты от системы не уходили в никуда (например, на оставленный включенным рабочий компьютер, когда человек уже дома). Во-вторых хранением сессий должен заняться Memcached, так как стандартные средства хранения сессий PHP очень любят дрючить винчестер, чего нам совсем не надо (или базу, что уже совсем за гранью добра и зла). Кроме того, ничего объемного в сессиях не будет, а для хранения неструктурированных настроек пользователя будет применяться Mongo, так что в плане экономии память все так же ОК.

Я бегло ознакомился с книгой на оф. сайте, небольшим кук-буком и несколькими статьями и не мудрствуя лукаво полез в код, разбираться, как оно все работает. Итак, все начинается с бутстрапа и объекта HttpKernel, в котором присутствует метод doHandle. Метод этот занят простым, но очень важным делом - изыскать ответ на запрос. Делает он это путем поэтапного запуска эвентов, на каждом из этапов приближаясь к заветной цели или же выдавая исключение, которое ловится и обрабатывается выше (метод handle).

В свете нашей задачи нас интересует первый этап и первй эвент, хапускаемый ядром:

// request
$event = new GetResponseEvent($this, $request, $type);
$this->dispatcher->dispatch(KernelEvents::REQUEST, $event);

Влезши грязными руками в диспатчер (ибо лень было рыть все конфиги), я обнаружил, что в этом месте этот самый диспатчер передаст евент следующим листнерам (в указанном порядке):

kernel.request => Symfony\Bundle\FrameworkBundle\EventListener\RouterListener :: onEarlyKernelRequest 
kernel.request => Symfony\Bundle\FrameworkBundle\EventListener\SessionListener :: onKernelRequest 
kernel.request => Symfony\Component\Security\Http\Firewall :: onKernelRequest 
kernel.request => Symfony\Bundle\FrameworkBundle\EventListener\RouterListener :: onKernelRequest 
kernel.exception => Symfony\Component\HttpKernel\EventListener\ExceptionListener :: onKernelException

Я отметил жирным шрифтом интересующий нас листнер. Дальнейшая наша задача - детально изучить код этого листнера и изучить конфиги сервиса, коим он является наряду со всеми остальными листнерами в symfony2.

Конфигурация сервисов, отвечающих за сессии может быть найдена в FrameworkBundle симфони в session.xml файле. В YAML-е (я предпочитаю именно этот формат конфигов) выглядит примерно так:

parameters:
  mje_core.auth_success_handler.class: MJE\CoreBundle\Component\Security\AuthenticationSuccessHandler
# Sessions
  session.class: Symfony\Component\HttpFoundation\Session
  session.storage.native.class: Symfony\Component\HttpFoundation\SessionStorage\NativeSessionStorage
  session.storage.filesystem.class: Symfony\Component\HttpFoundation\SessionStorage\FilesystemSessionStorage
  session_listener.class: Symfony\Bundle\FrameworkBundle\EventListener\SessionListener
    
services:
  security.authentication.success_handler:
    class: %mje_core.auth_success_handler.class%
    arguments: ['@router', '@security.user.entity_manager']
# Sessions
  
  session:
    class: %session.class%
    arguments: [@session.storage, %session.default_locale%]
    
  session.storage.native:
    class: %session.storage.native.class%
    arguments: [%session.storage.options%]       

  session.storage.filesystem:
    class: %session.storage.filesystem.class%
    arguments: [%kernel.cache_dir%/sessions, %session.storage.options%] 
  
  session_listener:
    class: %session_listener.class%
    tags:
      -  { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority:128 } 
    arguments: [@service_container]  

Проанализировав этот конфиг уже можно примерно понять что к чему, а точнее, что у нас как и с системой аутентификации есть глобальный SessionListener, который при надобности пытается стартовать сервис сессий Session, а тот в свою очередь хочет на вход первым параметром сервис SessionStorage, который будет отвечать за сохранение сессий.

Чтобы разобраться в том, какие параметры рулят выбором конкретной реализации SessionStorage (то есть кто же по факту будет сервисом session.storage) мы проанализируем метод registerSessionConfiguration в файле Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php. Там мы обнаружим список параметров, которые могут быть заданы в конфигурации сессий, среди них будет "storage_id" - это как раз то, что нам надо!

Можем указать конкретную имплементацию в config.php нашего приложения:

framework:
    router:   { resource: "%kernel.root_dir%/config/routing_dev.yml" }
#    router:   { resource: "%kernel.root_dir%/config/routing.yml" }
    profiler: { only_exceptions: false }
    session: storage_id: session.storage.filesystem

Итак, с цепочкой классов, отвечающих за сессии и их конфигурацией мы разобрались. В принципе таким способом можно разрыть любой сервис или подсистему симфони. Следующий шаг - выяснить в какой именно момент времени сессия становится не просто сессией некоего хрена с горы, но сессией нашего родного зарегистрированного юзера Васи Пупкина.

Заменив стандартный класс сессии (session.class) на свой с отладкой, я обнаружил, что в момент авторизации симфони ставит вот такого вида параметр в сессию:

_security_secured_area => C:74:"Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken":...

далее идет сериализованный токен. Имя аттрибута состоит из префикса "_security_" и имени моего файрвола "secured_area". В принципе уже сейчас можно добавить логику эксклюзивности сессии, отслеживая установку авторизационного токена, выдергивая из него объект пользователя и удаляя все его сессии, открытые на текущий момент. Но это попахивает совсем уж жестким хаком и может выйти боком, если вдруг будет изменено имя файрвола. Хотя в подавляющем большинстве случаев более одного файрвола в приложении нафиг не надо, все же не стоит закапывать в проект такую жирную потенциальную свинью, вместо этого мы поищем то место в коде, где непосредственно вызывается сеттер аттрибута сессии и посмотрим, что можно сделать там.

Итак, сеттер этого атрибута нашелся путем вывода трейса стека вызовов PHP, нашелся он в SecurityContext-е и вызывается он на событии Response, то есть тогда, когда ядро уже получило от контроллера объект ответа. Один из вариантов будет удалять все имеющиеся у пользователя лишние сессии именно тут. Но это чревато лишней нагрузкой, так как токен будет сохраняться symfony в сессию вне зависимости, логинился ли пользователь на протяжение данного запроса или же нет. То есть бОльшую часть времени мы будем искать лишние сессии там, где их взяться не могло.

На этом месте я остановился и оценил всю полученную информацию. Выводы вцелом не утешительные - малой кровью задачу не решить. и дальнейший путь выглядит так:

Пишем свою реализацию механизма хранения сессий SessionStorage

Пишем свой сервис по работе с сессией Session, который будет писать в сессию то и так, как нужно нам

Пишем свой AuthenticationListener, AuthenticationProvider, Token, чтобы передавать в токене не всего пользователя, а только его UID.

Рубрики: Symfony2

↑ Наверх


blog comments powered by Disqus

Контакты

Igor Zinkovsky aka TLoD,Snake. Писать на электропочту, стучаться в аську 302380533, искать в Санкт-Петербурге.

© 2002-2019