我目前正在使用
Symfony 2.1.8
和内置 PdoSessionHandler
。
我想在会话表中添加一个
user_id
字段来标识会话属于哪个(登录)用户。我的想法是,我可以强制用户重新登录以破坏他的会话。就我而言,如果更新用户的权限,就会发生这种情况。
我查看了内置的
PdoSessionHandler
,由于那些愚蠢的私有变量,你无法扩展它。
所以我尝试创建一个新的(复制/粘贴)并添加我的专栏
user_id
。
现在,如果用户未登录(匿名用户),此列可以为空。
所以我想把这个
user_id
写在handler的write方法中。用户已经存储在 $data
中,所以我想我可以检查该用户是否存在,抓住它的 id
并将其添加到插入/更新查询中。
问题是
$data
已编码 - 我猜是通过 session_encode() - 所以我不再确定这是处理我的新字段的最佳位置,但同时我看不到其他任何地方可以这样做,因为我需要更新此 MySQL 查询以插入新字段的值。
所以我的问题是:处理这个附加字段的最佳位置在哪里?还有这个user_id值怎么设置呢?
另一方面,真正令人烦恼的是,每次我登录或退出时,
Symfony
都会创建一个新的cookie。因此数据库最终会毫无意义地产生大量记录(始终是同一用户)。为什么 Symfony
不一直使用相同的 cookie 值?
您可以扩展 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 ]
我不确定修改会话是个好主意,您可以将会话 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();