如何向会话表添加自定义字段

问题描述 投票:0回答:2

我目前正在使用

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 值?

php session pdo symfony-2.1
2个回答
5
投票

您可以扩展 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 ]

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();
© www.soinside.com 2019 - 2024. All rights reserved.