目标
我们正在为客户提供一个多项选择练习系统的解决方案,学生每月支付会员资格,以测试他们的知识和准备医学相关的考试。在Symfony2中提供此解决方案的一个主要问题是,学生可以购买一个订阅,与同学和同事共享他们的凭据,并在多个并发登录中分摊订阅费用。
为了最小化这个问题,我们希望在Symfony2项目中避免同时维护多个会话。
大量的Google-fu引导我到这个稀疏的Google组线程,OP被简单地告知使用PdoSessionHandler将会话存储在数据库中。
这是另一个SO问题,其他人做了同样的事情,但没有解释如何去做。
目前进度
我已经为项目实现了这个处理程序,并且目前有一个security.interactive_login
侦听器,该侦听器将结果会话ID与数据库中的User存储在一起。进展在这里
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 SymfonyComponentSecurityCoreAuthenticationTokenUsernamePasswordToken(
$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
侦听器完成后的,因此User ID永远不会与会话表一起存储。我怎样才能使它工作?我可以将User ID存储在会话表中的什么位置?
或者,有没有更好的方法来做这件事?这对于Symfony来说是非常令人沮丧的,因为我不认为它的设计是为每个用户提供专属的单用户会话。
我已经解决了我自己的问题,但在我能够接受我自己的答案之前,我会把这个问题留给对话(如果有的话)。
我创建了一个kernel.request
监听器,它将在每次登录时检查用户的当前会话ID和与用户关联的最新会话ID。
代码如下:
<?php
namespace AcmeBundleListener;
use SymfonyComponentHttpKernelEventGetResponseEvent;
use SymfonyComponentHttpKernelHttpKernel;
use SymfonyComponentHttpFoundationRedirectResponse;
use SymfonyComponentSecurityCoreSecurityContext;
use SymfonyComponentDependencyInjectionContainer;
use SymfonyComponentRoutingRouter;
/**
* 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: AcmeBundleListenerSessionListener
arguments: ['@security.context', '@service_container', '@router']
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
有趣的是,当我意识到我之前将imcq.session.listener
命名为session_listener
时,我花了一段尴尬的时间想知道为什么侦听器使我的应用程序中断。原来Symfony(或其他一些bundle)已经使用了这个名字,因此我重写了它的行为。
小心!这将破坏FOSUserBundle 1.3.x的隐式登录功能。您应该升级到2.0。并使用其隐式登录事件,或者用您自己的fos_user.security.login_manager
服务替换LoginListener
。(我做了后者,因为我使用SonataUserBundle)
根据请求,以下是FOSUserBundle 1.3.x的完整解决方案:
对于隐式登录,将此添加到services.yml
:
fos_user.security.login_manager:
class: AcmeBundleSecurityLoginManager
arguments: ['@security.context', '@security.user_checker', '@security.authentication.session_strategy', '@service_container', '@doctrine']
在AcmeBundleSecurity
下创建一个名为LoginManager.php
的文件,代码为:
<?php
namespace AcmeBundleSecurity;
use FOSUserBundleSecurityLoginManagerInterface;
use FOSUserBundleModelUserInterface;
use SymfonyComponentDependencyInjectionContainerInterface;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentSecurityCoreAuthenticationTokenUsernamePasswordToken;
use SymfonyComponentSecurityCoreUserUserCheckerInterface;
use SymfonyComponentSecurityCoreSecurityContextInterface;
use SymfonyComponentSecurityHttpRememberMeRememberMeServicesInterface;
use SymfonyComponentSecurityHttpSessionSessionAuthenticationStrategyInterface;
use DoctrineBundleDoctrineBundleRegistry 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: AcmeBundleListenerLoginListener
arguments: ['@security.context', '@doctrine', '@service_container']
tags:
- { name: kernel.event_listener, event: security.interactive_login, method: onSecurityInteractiveLogin }
和后续的LoginListener.php
交互式登录事件:
<?php
namespace AcmeBundleListener;
use SymfonyComponentSecurityHttpEventInteractiveLoginEvent;
use SymfonyComponentSecurityCoreSecurityContext;
use SymfonyComponentDependencyInjectionContainer;
use DoctrineBundleDoctrineBundleRegistry as Doctrine; // for Symfony 2.1.0+
/**
* Custom login listener.
*/
class LoginListener
{
/** @var SymfonyComponentSecurityCoreSecurityContext */
private $securityContext;
/** @var DoctrineORMEntityManager */
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();
// ...
}
}