web-dev-qa-db-ja.com

PHPセッションデータをファイルシステムではなくデータベースに保存するにはどうすればよいですか?

2つのWebサイトがあります。1つはTLSで、もう1つは同じクライアント用ですが、両方とも users orders accounts など。

これは通常、$_SESSION dataですが、明らかにこれらは他のサイトでは機能しません。また、セッションデータをファイルシステムではなくデータベース(MySQL)に保存できることがわかりました。

私は周りを掘り下げて、 この便利なガイド とこの古いが 便利なガイド を見つけました。また、 このガイド が見つかりました。これは、MySQLが少し最新です。

インターフェイスクラスを作成しましたが、部分的にしか機能せず、セッションデータをデータベースに保存しますが、取得しません。また、 PHP manual からの推奨方法も使用しました。

My MySQL(上記のリンクの最初のカップルからコピーされた):

CREATE TABLE `sessions` (
  `id` varchar(32) COLLATE utf8_unicode_ci NOT NULL,
  `access` int(10) NOT NULL,
  `data` text COLLATE utf8_unicode_ci NOT NULL,
  UNIQUE KEY `id` (`id`)
) ENGINE=InnoDb DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

注意:インターフェイスクラスを紹介する前に、Db connetionは独自のカスタムインターフェイスを使用し、それ自体が完全に機能することを知っておいてください。

$sessionDBconnectionUrlには、メインWebサイトのコンテンツとは別のデータベースにセッションを保持しているため、セッションデータベース接続の詳細が含まれています。

インターフェイスクラス(上記のすべてのリンクに基づく)

<?php
/***
 * Created by PhpStorm.
 ***/
class HafSessionHandler implements SessionHandler {
    private $database = null;

    public function __construct($sessionDBconnectionUrl){

        if(!empty($sessionDBconnectionUrl) && file_exists($_SERVER['DOCUMENT_ROOT'].$sessionDBconnectionUrl)) {
            require_once "class.dataBase.php";
            // Instantiate new Database object
            $this->database = new Database($sessionDBconnectionUrl);
        }
        else {
            error_log("Session could not initialise class.");
        }

    }

    /**
     * Open
     */
    public function open($savepath, $id){
         $openRow = $this->database->getSelect("SELECT `data` FROM sessions WHERE id = ? LIMIT 1",$id);
    if($this->database->selectRowsFoundCounter() == 1){
        // Return True
        return $openRow['data'];
        }
    else {
        // Return False
        return ' ';
    }
    /**
     * Read
     */
    public function read($id)
    {
        // Set query
        $readRow = $this->database->getSelect('SELECT `data` FROM sessions WHERE id = ? LIMIT 1', $id,TRUE);
        if ($this->database->selectRowsFoundCounter() > 0) {
            return $readRow['data'];
        } else {
            error_log("could not read session id ".$id);
            return '';
        }
    }

    /**
     * Write
     */
    public function write($id, $data)
    {
        $access = time();
        // Set query
        $dataReplace[0] = $id;
        $dataReplace[1] = $access;
        $dataReplace[2] = $data;
        if ($this->database->noReturnQuery('REPLACE INTO sessions(id,access,`data`) VALUES (?, ?, ?)', $dataReplace)) {
            return TRUE;
        } else {
            return FALSE;
        }
    }

    /**
     * Destroy
     */
    public function destroy($id)
    {
        // Set query
        if ($this->database->noReturnQuery('DELETE * FROM sessions WHERE id = ? ', $id)) {
            return TRUE;
        } else {

            return FALSE;
        }
    }
    /**
     * Close
     */
    public function close(){
        // Close the database connection
        // If successful
        if($this->database->dbiLink->close){
            // Return True
            return true;
        }
        // Return False
        return false;
    }

    /**
     * Garbage Collection
     */
    public function gc($max)
    {
        // Calculate what is to be deemed old
        $old = time() - $max;

        // Set query
        if ($this->database->noReturnQuery('DELETE * FROM sessions WHERE access < ?', $old)) {
            return TRUE;
        } else {
            return FALSE;
        }
    }

    public function __destruct()
    {
        $this->close();
    }

}

マイテストページ(最初から書いた!)

<?php
require "class.sessionHandler.inc.php";
$HSH = new HafSessionHandler("connection.session.dbxlink.php");
session_set_save_handler( $HSH, TRUE );
session_start();

print "<p>Hello this is an index page</p>";
$_SESSION['horses'] = "treesx3";
$_SESSION['tiespan'] = (int)$_SESSION['tiespan']+7;

print "<p>There should be some session data in the database now. <a href='index3.php'>link</a></p>";
var_dump($_SESSION);


exit;

問題:

実行したテストページはデータをデータベースに保存しますが、データを取得していないようです。

エラーロギングを有効にしましたが、PHPエラーは報告されません。重大なMySQLエラーは報告されません。

なぜ機能しないのですか?

14
Martin

数時間のデバッグの過程で、多数のGoogle検索で参照された記事と、 herehere 、-などのStack Overflow回答の重要なサブセットが見つかったことを発見しました。 こちら すべてが無効または古い情報を提供します。

セッションデータをデータベースに保存する際に[重大な]問題を引き起こす可能性のあるもの:

  • オンラインのすべての例では_session_set_save_handler_を「入力」できると述べていますが、register_shutdown_function('session_write_close')も設定する必要があると述べているものはありません( reference )。

  • いくつかの(古い)ガイドは古いSQLデータベース構造を参照しているため、notは使用しないでください。セッションデータをデータベースに保存するために必要なデータベース構造は、id/access/dataです。それでおしまい。いくつかの「ガイド」と例で見たように、さまざまな追加のタイムスタンプ列は不要です。

    • 古いガイドのいくつかには、_DELETE * FROM ..._などの古いMySQL構文もあります
  • クラス[私の質問で作成]は、実装SessionHandlerInterfaceでなければなりません。適切なインターフェイスではないsessionHandlerの実装を提供するガイド(上記参照)を見てきました。おそらく、以前のバージョンのPHPはわずかに異なる方法(おそらく<5.4)でした。

  • セッションクラスメソッドmustは、PHPマニュアル。再び、おそらく5.4より前のPHPしかし、私が読んだ2つのガイドは_class->open_が読み込む行を返すと述べたのに対し、 PHPマニュアルの状態trueまたはfalseのみ。

  • これが元の問題の原因です:カスタムセッション名を使用していました(実際にはセッション名としてのIDとセッションIDこの非常に優れたStackOverflowポスト と同じことです!)これにより、128文字のセッション名が生成されました。セッション名は、セッションを危険にさらすためにクラックする必要がある唯一のキーであるため、 session hijacking を使用して、長い名前/ IDを使用することは非常に良いことです。

    • しかし、これはMySQLがセッションidを代わりにわずか32文字にスライスするため、問題を引き起こしました128なので、データベースでセッションデータを見つけることができませんでした。これは完全にサイレントな問題でした(おそらく、データベース接続クラスがそのようなことの警告をスローしないためです)。しかし、これは注意するべきものです。データベースからセッションを取得する際に問題がある場合、最初のチェックはfullセッションIDが指定されたフィールドに保存できることです。

そのため、邪魔にならないように、追加の詳細もいくつかあります。

PHPマニュアルページ(上記のリンク))は、クラスオブジェクトの不適切な行の山を示しています。

_$handler = new MySessionHandler();
session_set_save_handler($handler, true);
session_start();
_

一方、これをクラスコンストラクターに入れると同様に機能します。

_class MySessionHandler implements SessionHandlerInterface {

    private $database = null;

public function __construct(){

    $this->database = new Database(whatever);

    // Set handler to overide SESSION
    session_set_save_handler(
        array($this, "open"),
        array($this, "close"),
        array($this, "read"),
        array($this, "write"),
        array($this, "destroy"),
        array($this, "gc")
        );
    register_shutdown_function('session_write_close');
    session_start();
    }
...
}
_

これは、出力ページでセッションを開始するために必要なものは次のとおりであることを意味します:

_<?php
require "path/to/sessionhandler.class.php"; 
new MySessionHandler();

//Bang session has been setup and started and works
_

参考までに、完全なセッション通信クラスは次のとおりです。これはPHP 5.6(およびおそらく7ですが、7ではまだテストされていません)で動作します)

_<?php
/***
 * Created by PhpStorm.
 ***/
class MySessionHandler implements SessionHandlerInterface {
    private $database = null;

    public function __construct($sessionDBconnectionUrl){
        /***
         * Just setting up my own database connection. Use yours as you need.
         ***/ 

            require_once "class.database.include.php";
            $this->database = new DatabaseObject($sessionDBconnectionUrl);

        // Set handler to overide SESSION
        session_set_save_handler(
            array($this, "open"),
            array($this, "close"),
            array($this, "read"),
            array($this, "write"),
            array($this, "destroy"),
            array($this, "gc")
        );
        register_shutdown_function('session_write_close');
        session_start();
    }

    /**
     * Open
     */
    public function open($savepath, $id){
        // If successful
        $this->database->getSelect("SELECT `data` FROM sessions WHERE id = ? LIMIT 1",$id,TRUE);
        if($this->database->selectRowsFoundCounter() == 1){
            // Return True
            return true;
        }
        // Return False
        return false;
    }
    /**
     * Read
     */
    public function read($id)
    {
        // Set query
        $readRow = $this->database->getSelect('SELECT `data` FROM sessions WHERE id = ? LIMIT 1', $id,TRUE);
        if ($this->database->selectRowsFoundCounter() > 0) {
            return $readRow['data'];
        } else {
            return '';
        }
    }

    /**
     * Write
     */
    public function write($id, $data)
    {
        // Create time stamp
        $access = time();

        // Set query
        $dataReplace[0] = $id;
        $dataReplace[1] = $access;
        $dataReplace[2] = $data;
        if ($this->database->noReturnQuery('REPLACE INTO sessions(id,access,`data`) VALUES (?, ?, ?)', $dataReplace)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Destroy
     */
    public function destroy($id)
    {
        // Set query
        if ($this->database->noReturnQuery('DELETE FROM sessions WHERE id = ? LIMIT 1', $id)) {
            return true;
        } else {

            return false;
        }
    }
    /**
     * Close
     */
    public function close(){
        // Close the database connection
        if($this->database->dbiLink->close){
            // Return True
            return true;
        }
        // Return False
        return false;
    }

    /**
     * Garbage Collection
     */
    public function gc($max)
    {
        // Calculate what is to be deemed old
        $old = time() - $max;

        if ($this->database->noReturnQuery('DELETE FROM sessions WHERE access < ?', $old)) {
            return true;
        } else {
            return false;
        }
    }

    public function __destruct()
    {
        $this->close();
    }

}
_

使用法:クラスコードテキストのすぐ上に示すとおり。

24
Martin