2013-03-14 72 views
3

我目前使用的是Symfony 2.1.8和内置的PdoSessionHandler如何将自定义字段添加到会话表

我想在会话表中添加一个user_id字段来标识会话属于哪个(登录)用户。我的想法是,我可以强制用户重新登录销毁会话。在我的情况下,如果用户的权限被更新,将会发生。

我看了一下内置的PdoSessionHandler,你不能扩展,因为那些愚蠢的私有变量。

所以我试着创建一个新的(复制/粘贴)并添加我的列user_id。 如果用户未登录(匿名用户),此栏可以为空。

所以我想写这个user_id在处理程序的写入方法。该用户已存储在$data中,所以我想我可以检查该用户是否存在,获取它的id并将其添加到插入/更新查询中。

问题是$data被编码 - 我猜session_encode() - 所以我不能确定这是有史以来处理我的新领域的最好的地方,但同时我看不到其他地方我可以这样做,因为我需要更新这个MySQL查询来插入新字段的值。

所以我的问题是:哪里是最好的地方来处理这个额外的领域?以及如何设置此user_id值?

在另一个说明,somehing真的很烦人的是,Symfony正在创建一个新的cookie每次我登录或注销。因此,数据库最终会有很多记录(总是一样的用户)。为什么Symfony始终没有使用相同的cookie值?

回答

3

我不确定修改会话是个好主意,您可以将会话ID存储在用户实体中,并在需要时删除它们。这样,您可以确保只有用户每次只能登录一个会话。

完成它的最简单方法是使用登录监听器。

sessionId字段添加到用户实体(或文档或任何你使用的持久性):

// Acme/UserBundle/Entity/User.php 
namespace Acme\UserBundle\Entity; 

use Doctrine\ORM\Mapping as ORM; 

/** 
* ORM\Entity 
* @ORM\Table(name="fos_user") 
*/ 
class User { 
    // ... 

    /** 
    * @ORM\Column(name="session_id", type="string") 
    */ 
    private $sessionId; 

    public function getSessionId() { 
    return $this->sessionId; 
    } 

    public function setSessionId($sessionId = null) { 
    $this->sessionId = $sessionId; 
    return $this; 
    } 
} 

,并添加监听器:

namespace Dbla\UserBundle\Listener; 

use Symfony\Component\HttpFoundation\Session; 
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; 

class LoginListener 
{ 
    protected $doctrine; 

    protected $session; 

    public function __construct(Session $session, Registry $doctrine) 
    { 
    $this->doctrine = $doctrine; 
    $this->session = $session; 
    } 

    public function onLogin(InteractiveLoginEvent $event) 
    { 
    $user = $event->getAuthenticationToken()->getUser(); 

    if ($user) { 
     $user->setSessionId($this->session->getId()); 
     $em = $this->doctrine->getEntityManager(); 
     $em->persist($user); 
     $em->flush(); 
    } 
    } 
} 

并将其添加为一个服务:

services: 
    acme_user.listsner.login: 
    class: Acme\UserBundle\Listener\LoginListener 
    arguments: [ @session, @doctrine ] 
    tags: 
     - { name: kernel.event_listener, event: security.interactive_login, method: onLogin } 

然后您可以简单地删除用户的会话:

$users = []; // ... get user list 
$sessionIds = array_map(function($user) { 
    return $user->getId(); 
}); 
if (count(sessionIds) > 0) { 
    $sql = 'DELETE FROM session WHERE session_id IN (' . implode($sessionIds, ',') . ')'; 
    $entityManager->getConnection()->exec($sql); 
} 
foreach ($users as $user) { 
    $user->setSessionId(null); 
    $entityManager->persist($user); 
} 
$entityManager->flush(); 
+1

这不是一个坏主意,但...在会话表中拥有'user_id'我只需要执行一个查询:一个删除。另外我宁愿在会话表上执行查询,这意味着比用户表存储更少的记录(理论上),因为我应该只有登录用户的会话。我不太喜欢的是每个我想销毁一个会话,我需要做一个删除和更新用户。 – maxwell2022 2013-03-14 13:27:42

+0

从用户中删除'sessionId'也可以在一个查询中完成。但是这样你可以注册监听器来在用户会话被删除时执行额外的任务('用户'实体被修改)。它仅被刷新一次,因此只有最小的性能影响。 – 2013-03-18 17:35:16

+0

取决于您的应用程序字符,但通常会有比认证的更多的匿名会话。匿名用户也有会话。 – 2013-03-18 17:38:04

4

你可以扩展PdoSessionHandler(解决方案> = Symfony的2.1):

namespace Acme\DemoBundle\HttpFoundation\Session\Storage\Handler; 

use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; 
use Symfony\Component\Security\Core\SecurityContext; 

class UserIdPdoSessionHandler extends PdoSessionHandler 
{ 
    /** 
    * @var \PDO PDO instance. 
    */ 
    private $pdo; 

    /** 
    * @var array Database options. 
    */ 
    private $dbOptions; 

    /** 
    * @var SecurityContext 
    */ 
    private $context; 

    public function __construct(\PDO $pdo, array $dbOptions = array(), SecurityContext $context) 
    { 
     $this->pdo = $pdo; 
     $this->dbOptions = array_merge(
      array('db_user_id_col' => 'user_id'), 
      $dbOptions 
     ); 
     $this->context = $context; 

     parent::__construct($pdo, $dbOptions); 
    } 

    public function read($id) 
    { 
     // get table/columns 
     $dbTable = $this->dbOptions['db_table']; 
     $dbDataCol = $this->dbOptions['db_data_col']; 
     $dbIdCol = $this->dbOptions['db_id_col']; 

     try { 
      $sql = "SELECT $dbDataCol FROM $dbTable WHERE $dbIdCol = :id"; 

      $stmt = $this->pdo->prepare($sql); 
      $stmt->bindParam(':id', $id, \PDO::PARAM_STR); 

      $stmt->execute(); 
      // it is recommended to use fetchAll so that PDO can close the DB cursor 
      // we anyway expect either no rows, or one row with one column. fetchColumn, seems to be buggy #4777 
      $sessionRows = $stmt->fetchAll(\PDO::FETCH_NUM); 

      if (count($sessionRows) == 1) { 
       return base64_decode($sessionRows[0][0]); 
      } 

      // session does not exist, create it 
      $this->createNewSession($id); 

      return ''; 
     } catch (\PDOException $e) { 
      throw new \RuntimeException(sprintf('PDOException was thrown when trying to read the session data: %s', $e->getMessage()), 0, $e); 
     } 
    } 

    /** 
    * {@inheritDoc} 
    */ 
    public function write($id, $data) 
    { 
     // get table/column 
     $dbTable  = $this->dbOptions['db_table']; 
     $dbDataCol = $this->dbOptions['db_data_col']; 
     $dbIdCol  = $this->dbOptions['db_id_col']; 
     $dbTimeCol = $this->dbOptions['db_time_col']; 
     $dbUserIdCol = $this->dbOptions['db_user_id_col']; 

     //session data can contain non binary safe characters so we need to encode it 
     $encoded = base64_encode($data); 

     $userId = $this->context->isGranted('IS_AUTHENTICATED_REMEMBERED') ? 
      $this->context->getToken()->getUser()->getId() : 
      null 
     ; 

     try { 
      $driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); 

      if ('mysql' === $driver) { 
       // MySQL would report $stmt->rowCount() = 0 on UPDATE when the data is left unchanged 
       // it could result in calling createNewSession() whereas the session already exists in 
       // the DB which would fail as the id is unique 
       $stmt = $this->pdo->prepare(
        "INSERT INTO $dbTable ($dbIdCol, $dbDataCol, $dbTimeCol, $dbUserIdCol) VALUES (:id, :data, :time, :user_id) " . 
        "ON DUPLICATE KEY UPDATE $dbDataCol = VALUES($dbDataCol), $dbTimeCol = VALUES($dbTimeCol)" 
       ); 
       $stmt->bindParam(':id', $id, \PDO::PARAM_STR); 
       $stmt->bindParam(':data', $encoded, \PDO::PARAM_STR); 
       $stmt->bindValue(':time', time(), \PDO::PARAM_INT); 
       $stmt->bindParam(':user_id', $userId, \PDO::PARAM_STR); 
       $stmt->execute(); 
      } elseif ('oci' === $driver) { 
       $stmt = $this->pdo->prepare("MERGE INTO $dbTable USING DUAL ON($dbIdCol = :id) ". 
         "WHEN NOT MATCHED THEN INSERT ($dbIdCol, $dbDataCol, $dbTimeCol, $dbUserIdCol) VALUES (:id, :data, sysdate, :user_id) " . 
         "WHEN MATCHED THEN UPDATE SET $dbDataCol = :data WHERE $dbIdCol = :id"); 

       $stmt->bindParam(':id', $id, \PDO::PARAM_STR); 
       $stmt->bindParam(':data', $encoded, \PDO::PARAM_STR); 
       $stmt->bindParam(':user_id', $userId, \PDO::PARAM_STR); 
       $stmt->execute(); 
      } else { 
       $stmt = $this->pdo->prepare("UPDATE $dbTable SET $dbDataCol = :data, $dbTimeCol = :time WHERE $dbIdCol = :id"); 
       $stmt->bindParam(':id', $id, \PDO::PARAM_STR); 
       $stmt->bindParam(':data', $encoded, \PDO::PARAM_STR); 
       $stmt->bindValue(':time', time(), \PDO::PARAM_INT); 
       $stmt->execute(); 

       if (!$stmt->rowCount()) { 
        // No session exists in the database to update. This happens when we have called 
        // session_regenerate_id() 
        $this->createNewSession($id, $data); 
       } 
      } 
     } catch (\PDOException $e) { 
       throw new \RuntimeException(sprintf('PDOException was thrown when trying to write the session data: %s', $e->getMessage()), 0, $e); 
     } 

     return true; 
    } 

    private function createNewSession($id, $data = '') 
    { 
     // get table/column 
     $dbTable  = $this->dbOptions['db_table']; 
     $dbDataCol = $this->dbOptions['db_data_col']; 
     $dbIdCol  = $this->dbOptions['db_id_col']; 
     $dbTimeCol = $this->dbOptions['db_time_col']; 
     $dbUserIdCol = $this->dbOptions['db_user_id_col']; 

     $userId = $this->context->isGranted('IS_AUTHENTICATED_REMEMBERED') ? 
      $this->context->getToken()->getUser()->getId() : 
      null 
     ; 

     $sql = "INSERT INTO $dbTable ($dbIdCol, $dbDataCol, $dbTimeCol, $dbUserIdCol) VALUES (:id, :data, :time, :user_id)"; 

     //session data can contain non binary safe characters so we need to encode it 
     $encoded = base64_encode($data); 
     $stmt = $this->pdo->prepare($sql); 
     $stmt->bindParam(':id', $id, \PDO::PARAM_STR); 
     $stmt->bindParam(':data', $encoded, \PDO::PARAM_STR); 
     $stmt->bindValue(':time', time(), \PDO::PARAM_INT); 
     $stmt->bindParam(':user_id', $userId, \PDO::PARAM_STR); 
     $stmt->execute(); 

     return true; 
    } 
} 

而且配置会话使用它:

# config.yml 
framework: 
    session: 
     # ... 
     handler_id: session.storage.custom 

parameters: 
    pdo.db_options: 
     db_table:  session 
     db_id_col:  session_id 
     db_data_col: session_value 
     db_time_col: session_time 
     db_user_id_col: session_user_id 

services: 
    pdo: 
     class: PDO 
     arguments: 
      dsn:  "mysql:host=%database_host%;dbname=%database_name%" 
      user:  "%database_user%" 
      password: "%database_password%" 

    session.storage.custom: 
     class: Acme\DemoBundle\HttpFoundation\Session\Storage\Handler\UserIdPdoSessionHandler 
     arguments: [ @pdo, "%pdo.db_options%", @security.context ] 
+0

你的代码似乎有错误。构造函数应该以'$ this-> dbOptions'开始,而不仅仅是'dbOptions'。 – Maerlyn 2013-07-17 13:26:31

+0

谢谢,修复。 – 2013-07-29 19:39:19

+0

代码中的其他错误:'$ securityContext'未定义:( – 2013-11-21 13:37:43

相关问题