4

私は現在Symfony 2.1.8、ビルトインを使用していPdoSessionHandlerます。

user_idセッション テーブルにフィールドを追加して、セッションが属する (ログインしている) ユーザーを識別したいと考えています。アイデアは、セッションを破棄する際にユーザーに強制的にログインし直すことができるということです。私の場合、ユーザーの権限が更新された場合に発生します。

PdoSessionHandlerこれらのばかげたプライベート変数のために拡張できないビルドインを調べました。

そこで、新しいものを作成して(コピー/貼り付け)、列を追加しようとしましたuser_id。ユーザーがログインしていない場合 (匿名ユーザー)、この列は null になる可能性があります。

user_idそこで、これをハンドラの write メソッドに書きたいと思います。ユーザーは既にに保存されている$dataので、このユーザーが存在するかどうかを確認し、それを取得しidて挿入/更新クエリに追加できると考えていました。

問題は、それ$dataがエンコードされていることです-session_encode()によると思います-したがって、これが新しいフィールドを処理するのにこれまでで最高の場所であるかどうかはわかりませんが、同時に、他の場所でそれを行うことができません新しいフィールドの値を挿入するには、この MySQL クエリを更新する必要があります。

だから私の質問は、この追加フィールドを処理するのに最適な場所はどこですか? そして、この user_id 値を設定する方法は?

別のメモとして、本当に厄介なのは、Symfonyログインまたはログアウトするたびに新しい Cookie を作成することです。そのため、データベースには無駄に大量のレコードが作成されます (常に同じユーザーです)。Symfony常に同じ Cookie 値を使用しないのはなぜですか?

4

2 に答える 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 ]
于 2013-03-26T10:00:30.127 に答える
3

セッションの変更が良いアイデアかどうかはわかりません。代わりにセッション ID をユーザー エンティティに保存し、必要に応じて削除することができます。このようにして、たとえば、一度に 1 つのセッションのみでユーザーのみがログインできるようにすることができます。

これを実現する最も簡単な方法は、ログイン リスナーを使用することです。

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();
于 2013-03-14T12:53:22.750 に答える