Php 防止Symfony2中同时出现用户会话 目标

Php 防止Symfony2中同时出现用户会话 目标,php,symfony-2.5,Php,Symfony 2.5,我们为客户提供多项选择练习系统的解决方案,学生每月支付会员费,以测试他们的知识,并为医学相关考试做准备。在Symfony2中提供此解决方案的一个主要问题是,学生可以购买一个订阅,与同学和同事共享其凭据,并将订阅成本分摊到多个并发登录上 为了最大限度地减少此问题,我们希望防止在Symfony2项目中同时维护多个会话。 研究 大量的GoogleFu让我找到了这个稀疏的地方,OP被告知使用PdoSessionHandler将会话存储在数据库中 这里有一些,但没有解释如何做 迄今取得的进展 我已经为项目

我们为客户提供多项选择练习系统的解决方案,学生每月支付会员费,以测试他们的知识,并为医学相关考试做准备。在Symfony2中提供此解决方案的一个主要问题是,学生可以购买一个订阅,与同学和同事共享其凭据,并将订阅成本分摊到多个并发登录上

为了最大限度地减少此问题,我们希望防止在Symfony2项目中同时维护多个会话。

研究 大量的GoogleFu让我找到了这个稀疏的地方,OP被告知使用PdoSessionHandler将会话存储在数据库中

这里有一些,但没有解释如何做

迄今取得的进展 我已经为项目实现了这个处理程序,目前有一个
security.interactive\u login
侦听器,它将生成的会话ID与用户一起存储在数据库中。进展就在这里

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
}
public function\uu构造(SecurityContext$SecurityContext,条令$条令,容器$Container)
{
$this->securityContext=$securityContext;
$this->doc=$doctor;
$this->em=$doctrine->getManager();
$this->container=$container;
}
/**
*施展魔法。
* 
*@param InteractiveEngineent$事件
*/
安全性Interactivelogin(InteractiveEngineent$事件)上的公共函数
{
如果($this->securityContext->isgranded('IS\u AUTHENTICATED\u FULLY')){
//用户刚刚登录
}
如果($this->securityContext->isgranded('IS\u AUTHENTICATED\u membered')){
//用户已使用“记住我”cookie登录
}
//首先获取该用户对象,以便我们可以使用它
$user=$event->getAuthenticationToken()->getUser();
//现在检查一下他们是否是订户
如果($this->securityContext->isgranded('ROLE\u SUBSCRIBED')){
//检查它们的有效期与现在的有效期
如果($user->getExpiry()removole('ROLE_SUBSCRIBED');
$this->em->persist($user);
$this->em->flush();
//现在我们已经删除了他们的角色,我们必须创建一个新的令牌并将其加载到会话中
$token=new\Symfony\Component\Security\Core\Authentication\token\UsernamePasswordToken(
$user,
无效的
"主要",,
$user->getRoles()
);
$this->securityContext->setToken($token);
}
}
//获取当前会话并将用户与其关联
$sessionId=$this->container->get('session')->getId();
$user->setSessionId($sessionId);
$this->em->persist($user);
$s=$this->doc->getRepository('imcqBundle:Session')->find($sessionId);
如果($s){/$s=false,则此部分不执行
$s->setUserId($user->getId());
$this->em->persist($s);
}
$this->em->flush();
//我们现在必须注销当前会话令牌之外共享相同用户名的所有其他用户
//…在这段代码中,我将分离所有其他具有userId=当前登录用户的`imcqBundle:Session`实体
}
问题 直到
安全性.interactive_login
侦听器完成后,会话才会从PdoSessionHandler存储到数据库中,因此用户ID永远不会与会话表一起存储我如何才能使这项工作成功?会话表中的用户ID存储在哪里?


或者,有没有更好的办法?这对Symfony来说是非常令人沮丧的,因为我认为它从来没有为每个用户设计过专用的单用户会话。

我已经解决了自己的问题,但在我能够接受自己的答案之前,将把问题留给对话(如果有的话)

我创建了一个
kernel.request
监听器,它将在每次登录时检查用户的当前会话ID和与用户关联的最新会话ID

代码如下:

<?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;
                }
            }
        }
    }
}
<?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());
    }
}
有趣的是,当我意识到我以前将
imcq.session.listener
命名为
session\u listener
时,我花了大量令人尴尬的时间想知道为什么我的侦听器让我的应用程序中断。原来Symfony(或其他包)已经在使用这个名称,因此我重写了它的行为

小心这将破坏FOSUserBundle 1.3.x上的隐式登录功能。您应该升级到2.0.x-dev并使用其隐式登录事件,或者用您自己的
fos\u user.security.login\u manager
服务替换
LoginListener
。(我之所以选择后者,是因为我使用的是SonataUserBundle)

根据要求,以下是FOSUserBundle 1.3.x的完整解决方案:

对于隐式登录,请将其添加到您的
服务中。yml

fos_user.security.login_manager:
    class: Acme\Bundle\Security\LoginManager
    arguments: ['@security.context', '@security.user_checker', '@security.authentication.session_strategy', '@service_container', '@doctrine']
login_listener:
    class: Acme\Bundle\Listener\LoginListener
    arguments: ['@security.context', '@doctrine', '@service_container']
    tags:
        - { name: kernel.event_listener, event: security.interactive_login, method: onSecurityInteractiveLogin }
并在
Acme\Bundle\Security
下创建一个名为
LoginManager.php
的文件,代码如下:

<?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;
                }
            }
        }
    }
}
<?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());
    }
}
以及用于交互式登录事件的后续
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();

        // ...
    }
}

因此,您的数据库中有以下结构:学生可以购买一个订阅,与同学和同事共享他们的凭据,并将订阅成本分摊到多个并发登录上。请显示实现此结构的数据库表。您使用什么查询来维护它们?使用会话的问题是“它们是临时的”,不会记录在任何永久的地方。尤其是对于一组可能不同时处于活动状态的“用户”。@RyanVincent用户表是从Sonata用户包(扩展了FOSUserBundle)创建的。所有这些都是默认行为-我无需向您展示FOSUserBundle中的实体和功能