Мы предоставляем клиенту решение для системы множественного выбора, в которой студенты платят за ежемесячное членство, чтобы проверить свои знания и подготовиться к медицинским экзаменам. Основная проблема с предоставлением этого решения в Symfony2 заключается в том, что учащиеся могут купить одну подписку, поделиться своими учетными данными с одноклассниками и коллегами и разделить стоимость подписки на несколько параллельных логинов.
Чтобы свести к минимуму эту проблему, мы хотим помешать проведению более одного одновременного сеанса в нашем проекте Symfony2.
Массовые суммы Google-fu привели меня к этой редкой группе Google, где OP было кратко сказано использовать PdoSessionHandler для хранения сеансов в базе данных.
Вот еще один вопрос, когда кто-то другой работал вокруг одного и того же , но не объяснил, как это сделать.
Я реализовал этот обработчик для проекта и в настоящее время имеет прослушиватель security.interactive_login
который хранит результирующий идентификатор сеанса с пользователем в базе данных. Прогресс здесь
public function __construct(SecurityContext $securityContext, Doctrine $doctrine, Container $container) { $this->securityContext = $securityContext; $this->doc = $doctrine; $this->em = $doctrine->getManager(); $this->container = $container; } /** * Do the magic. * * @param InteractiveLoginEvent $event */ public function onSecurityInteractiveLogin(InteractiveLoginEvent $event) { if ($this->securityContext->isGranted('IS_AUTHENTICATED_FULLY')) { // user has just logged in } if ($this->securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) { // user has logged in using remember_me cookie } // First get that user object so we can work with it $user = $event->getAuthenticationToken()->getUser(); // Now check to see if they're a subscriber if ($this->securityContext->isGranted('ROLE_SUBSCRIBED')) { // Check their expiry date versus now if ($user->getExpiry() < new \DateTime('now')) { // If the expiry date is past now, we need to remove their role $user->removeRole('ROLE_SUBSCRIBED'); $this->em->persist($user); $this->em->flush(); // Now that we've removed their role, we have to make a new token and load it into the session $token = new \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken( $user, null, 'main', $user->getRoles() ); $this->securityContext->setToken($token); } } // Get the current session and associate the user with it $sessionId = $this->container->get('session')->getId(); $user->setSessionId($sessionId); $this->em->persist($user); $s = $this->doc->getRepository('imcqBundle:Session')->find($sessionId); if ($s) { // $s = false, so this part doesn't execute $s->setUserId($user->getId()); $this->em->persist($s); } $this->em->flush(); // We now have to log out all other users that are sharing the same username outside of the current session token // ... This is code where I would detach all other `imcqBundle:Session` entities with a userId = currently logged in user }
Сеанс не сохраняется в базе данных из PdoSessionHandler до тех пор, пока слушатель security.interactive_login
будет завершен, поэтому идентификатор пользователя никогда не заканчивается тем, что он сохраняется в таблице сеанса. Как я могу сделать эту работу? Где я могу хранить хранилище идентификаторов пользователей в таблице сеансов?
Альтернативно, есть ли лучший способ обойти это? Это очень расстраивает Symfony, поскольку я не думаю, что он когда-либо создавал эксклюзивные сеансы пользователя для каждого пользователя.
Я решил свою проблему, но оставил вопрос открытым для диалога (если есть), прежде чем я смогу принять свой собственный ответ.
Я создал слушатель kernel.request
который будет проверять текущий идентификатор сеанса пользователя с последним идентификатором сеанса, связанным с пользователем при каждом входе в систему.
Вот код:
<?php namespace Acme\Bundle\Listener; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\Security\Core\SecurityContext; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Routing\Router; /** * Custom session listener. */ class SessionListener { private $securityContext; private $container; private $router; public function __construct(SecurityContext $securityContext, Container $container, Router $router) { $this->securityContext = $securityContext; $this->container = $container; $this->router = $router; } public function onKernelRequest(GetResponseEvent $event) { if (!$event->isMasterRequest()) { return; } if ($token = $this->securityContext->getToken()) { // Check for a token - or else isGranted() will fail on the assets if ($this->securityContext->isGranted('IS_AUTHENTICATED_FULLY') || $this->securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) { // Check if there is an authenticated user // Compare the stored session ID to the current session ID with the user if ($token->getUser() && $token->getUser()->getSessionId() !== $this->container->get('session')->getId()) { // Tell the user that someone else has logged on with a different device $this->container->get('session')->getFlashBag()->set( 'error', 'Another device has logged on with your username and password. To log back in again, please enter your credentials below. Please note that the other device will be logged out.' ); // Kick this user out, because a new user has logged in $this->securityContext->setToken(null); // Redirect the user back to the login page, or else they'll still be trying to access the dashboard (which they no longer have access to) $response = new RedirectResponse($this->router->generate('sonata_user_security_login')); $event->setResponse($response); return $event; } } } } }
и запись services.yml
:
services: acme.session.listener: class: Acme\Bundle\Listener\SessionListener arguments: ['@security.context', '@service_container', '@router'] tags: - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
Интересно отметить, что я потратил смущающее количество времени на то, чтобы узнать, почему мой слушатель нарушил мое приложение, когда понял, что ранее я назвал imcq.session.listener
session_listener
. Оказывается, Symfony (или какой-то другой пакет) уже использовал это имя, и поэтому я переоценивал его поведение.
Быть осторожен! Это нарушит неявные функции входа в FOSUserBundle 1.3.x. Вы должны либо перейти на 2.0.x-dev, либо использовать его неявное событие входа или заменить LoginListener
своим собственным fos_user.security.login_manager
. (Я сделал последнее, потому что я использую SonataUserBundle)
По запросу, вот полное решение для FOSUserBundle 1.3.x:
Для неявных логинов добавьте это в свои services.yml
:
fos_user.security.login_manager: class: Acme\Bundle\Security\LoginManager arguments: ['@security.context', '@security.user_checker', '@security.authentication.session_strategy', '@service_container', '@doctrine']
И создайте файл под Acme\Bundle\Security
именем LoginManager.php
с кодом:
<?php namespace Acme\Bundle\Security; use FOS\UserBundle\Security\LoginManagerInterface; use FOS\UserBundle\Model\UserInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\SecurityContextInterface; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface; use Doctrine\Bundle\DoctrineBundle\Registry as Doctrine; // for Symfony 2.1.0+ class LoginManager implements LoginManagerInterface { private $securityContext; private $userChecker; private $sessionStrategy; private $container; private $em; public function __construct(SecurityContextInterface $context, UserCheckerInterface $userChecker, SessionAuthenticationStrategyInterface $sessionStrategy, ContainerInterface $container, Doctrine $doctrine) { $this->securityContext = $context; $this->userChecker = $userChecker; $this->sessionStrategy = $sessionStrategy; $this->container = $container; $this->em = $doctrine->getManager(); } final public function loginUser($firewallName, UserInterface $user, Response $response = null) { $this->userChecker->checkPostAuth($user); $token = $this->createToken($firewallName, $user); if ($this->container->isScopeActive('request')) { $this->sessionStrategy->onAuthentication($this->container->get('request'), $token); if (null !== $response) { $rememberMeServices = null; if ($this->container->has('security.authentication.rememberme.services.persistent.'.$firewallName)) { $rememberMeServices = $this->container->get('security.authentication.rememberme.services.persistent.'.$firewallName); } elseif ($this->container->has('security.authentication.rememberme.services.simplehash.'.$firewallName)) { $rememberMeServices = $this->container->get('security.authentication.rememberme.services.simplehash.'.$firewallName); } if ($rememberMeServices instanceof RememberMeServicesInterface) { $rememberMeServices->loginSuccess($this->container->get('request'), $response, $token); } } } $this->securityContext->setToken($token); // Here's the custom part, we need to get the current session and associate the user with it $sessionId = $this->container->get('session')->getId(); $user->setSessionId($sessionId); $this->em->persist($user); $this->em->flush(); } protected function createToken($firewall, UserInterface $user) { return new UsernamePasswordToken($user, null, $firewall, $user->getRoles()); } }
Для более важных интерактивных логинов вы также должны добавить это в свои services.yml
:
login_listener: class: Acme\Bundle\Listener\LoginListener arguments: ['@security.context', '@doctrine', '@service_container'] tags: - { name: kernel.event_listener, event: security.interactive_login, method: onSecurityInteractiveLogin }
и последующие LoginListener.php
для интерактивных событий входа:
<?php namespace Acme\Bundle\Listener; use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; use Symfony\Component\Security\Core\SecurityContext; use Symfony\Component\DependencyInjection\Container; use Doctrine\Bundle\DoctrineBundle\Registry as Doctrine; // for Symfony 2.1.0+ /** * Custom login listener. */ class LoginListener { /** @var \Symfony\Component\Security\Core\SecurityContext */ private $securityContext; /** @var \Doctrine\ORM\EntityManager */ private $em; private $container; private $doc; /** * Constructor * * @param SecurityContext $securityContext * @param Doctrine $doctrine */ public function __construct(SecurityContext $securityContext, Doctrine $doctrine, Container $container) { $this->securityContext = $securityContext; $this->doc = $doctrine; $this->em = $doctrine->getManager(); $this->container = $container; } /** * Do the magic. * * @param InteractiveLoginEvent $event */ public function onSecurityInteractiveLogin(InteractiveLoginEvent $event) { if ($this->securityContext->isGranted('IS_AUTHENTICATED_FULLY')) { // user has just logged in } if ($this->securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) { // user has logged in using remember_me cookie } // First get that user object so we can work with it $user = $event->getAuthenticationToken()->getUser(); // Get the current session and associate the user with it //$user->setSessionId($this->securityContext->getToken()->getCredentials()); $sessionId = $this->container->get('session')->getId(); $user->setSessionId($sessionId); $this->em->persist($user); $this->em->flush(); // ... } }