如何将会话数据保存到数据库而不是文件系统中?

时间:2021-09-15 07:10:40

I have two websites, One is TLS and one is not, both are for the same client but I need the websites to share with each other (and only each other) common data for users, orders, accounts etc.

我有两个网站,一个是TLS,另一个不是,都是同一个客户端,但是我需要这些网站互相分享(而且只有彼此)用户、订单、账户等公共数据。

This would normally be done with $_SESSION data but I obviously these can't work across other sites, and I have found that I can store session data in a database (MySQL) rather than in the file system.

这通常是用$_SESSION数据完成的,但是显然这些不能在其他站点上工作,而且我发现我可以在数据库(MySQL)而不是在文件系统中存储会话数据。

I have dug around and found This useful guide as well as this older but useful guide. I also found this guide which has slightly more up to date MySQL.

我翻遍了所有的地方,找到了这本有用的指南,也找到了这本古老但有用的指南。我还找到了这个指南,它有一些最新的MySQL。

I have written an interface class but it only partly works, it stores the session data in the database, but it doesn't retrieve it. I have also used the suggested method from the PHP manual.

我已经编写了一个接口类,但它只能部分工作,它将会话数据存储在数据库中,但它不检索它。我还使用了PHP手册中建议的方法。

My MySQL (as copied from first couple of the above links):

我的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;

Please Note: Before I show you my interface class please know that the Db connetion uses my own custom made interface and that works perfectly, in itself.

请注意:在我向您展示我的接口类之前,请知道Db connetion使用了我自己定制的接口,这本身就很完美。

The $sessionDBconnectionUrl contains the Session Database connection details as I am keeping sessions on a seperate Database from the main website contents.

$sessionDBconnectionUrl包含会话数据库连接的详细信息,因为我将会话保存在一个独立的数据库上,与主网站内容分离。

My interface class (as based on all the above links)

My interface类(基于以上所有链接)

<?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();
    }

}

My Test Page (written from scratch!)

我的测试页面(从头开始编写!)

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

Issue:

问题:

The test pages I run save the data to the database ok but they do not seem to retrieve the data,

我运行的测试页面将数据保存到数据库中但它们似乎不检索数据,

I have error logging enabled and no PHP errors are reported. No critical MySQL errors are reported.

我启用了错误日志记录,并且没有报告PHP错误。没有严重的MySQL错误报告。

Why doesn't it work?

为什么它不工作?

1 个解决方案

#1


14  

I have found over the course of several hours debugging that the referenced articles found on numerous Google searches as well as a significant subset of Stack Overflow answers such as here, here and here all provide invalid or outdated information.

我在几个小时的调试过程中发现,在许多谷歌搜索中找到的参考文章以及大量的Stack Overflow的答案,如这里,这里和这里都提供无效或过时的信息。

Things that can cause [critical] issues with saving session data to a database:

将会话数据保存到数据库中可能引起[关键]问题的事情:

  • While all the examples online state that you can "fill" the session_set_save_handler, none of them state that you must also set the register_shutdown_function('session_write_close') too (reference).

    虽然所有的在线示例都表明您可以“填充”session_set_save_handler,但是没有一个实例表明您还必须设置register_shutdown_function('session_write_close')(引用)。

  • Several (older) guides refer to an outdated SQL Database structure, and should not be used. The database structure that you need for saving session data to the database is: id/access/data. That's it. no need for various extra timestamp columns as I've seen on a few "guides" and examples.

    一些(较老的)指南引用过时的SQL数据库结构,不应该使用。将会话数据保存到数据库所需的数据库结构是:id/access/data。就是这样。不需要各种额外的时间戳列,正如我在一些“指南”和示例中看到的那样。

    • Several of the older guides also have outdated MySQL syntax such as DELETE * FROM ...
    • 一些较老的指南也有过时的MySQL语法,比如从……
  • The class [made in my question] must implement the SessionHandlerInterface . I have seen guides (referenced above) that give the implementation of sessionHandler which is not a suitable interface. Perhaps previous versions of PHP had a slightly different method (probably <5.4).

    类(在我的问题中创建)必须实现SessionHandlerInterface。我已经看到了给sessionHandler的实现提供了一个不合适的接口的指南(上面提到过)。可能以前的PHP版本有一个稍微不同的方法(可能<5.4)。

  • The session class methods must return the values set out by the PHP manual. Again, probably inherited from pre-5.4 PHP but two guides I read stated that class->open returns the row to be read, whereas the PHP manual states that it needs to return true or false only.

    会话类方法必须返回PHP手册中设置的值。同样,可能继承自pre-5.4 PHP,但我读过的两个指南指出,class->open返回要读取的行,而PHP手册声明它只需要返回true或false。

  • This is the cause of my Original Issue: I was using custom session names (actually id's as session names and session id's are the same thing!) as per this very good * post and this was generating a session name that was 128 characters long. As the session name is the sole key that is needed to be cracked to compromise a session and take over with a session hijacking then a longer name/id is a very good thing.

    这就是我最初问题的原因:我使用自定义会话名称(实际上id作为会话名称,会话id是一样的!)由于会话名是惟一需要破解的密钥,因此需要使用较长的名称/id来破坏会话并接管会话。

    • But, this caused an issue because MySQL was silently slicing the session id down to just 32 characters instead of 128, so it was never able to find the session data in the database. This was a completely silent issue (maybe due to my database connection class not throwing warnings of such things). But this is the one to watch out for. If you have any issues with retrieving sessions from a database first check is that the full session id can be stored in the field provided.
    • 但是,这引起了一个问题,因为MySQL将会话id悄悄地分割为32个字符,而不是128个字符,因此无法在数据库中找到会话数据。这是一个完全无声的问题(可能是由于我的数据库连接类没有抛出此类问题的警告)。但这是值得注意的。如果您对从数据库检索会话有任何问题,那么首先要检查的是,完整的会话id可以存储在提供的字段中。

So with all that out of the way there are some extra details to add as well:

因此,除了这些之外,还有一些额外的细节需要补充:

The PHP manual page (linked above) shows an unsuitable pile of lines for a class object:

PHP手册页面(链接在上面)显示了一堆不适合类对象的行:

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

Whereas it works just as well if you put this in the class constructor:

然而,如果你把它放在类构造函数中,它也同样有效:

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();
    }
...
}

This means that to then start a session on your output page all you need is:

这意味着在您的输出页面上启动一个会话,您所需要的就是:

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

//Bang session has been setup and started and works

For reference the complete Session communication class is as follows, this works with PHP 5.6 (and probably 7 but not tested on 7 yet)

为了引用完整的会话通信类,如下所示,这适用于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();
    }

}

Usage: As shown just above the class code text.

用法:如类代码文本上方所示。

#1


14  

I have found over the course of several hours debugging that the referenced articles found on numerous Google searches as well as a significant subset of Stack Overflow answers such as here, here and here all provide invalid or outdated information.

我在几个小时的调试过程中发现,在许多谷歌搜索中找到的参考文章以及大量的Stack Overflow的答案,如这里,这里和这里都提供无效或过时的信息。

Things that can cause [critical] issues with saving session data to a database:

将会话数据保存到数据库中可能引起[关键]问题的事情:

  • While all the examples online state that you can "fill" the session_set_save_handler, none of them state that you must also set the register_shutdown_function('session_write_close') too (reference).

    虽然所有的在线示例都表明您可以“填充”session_set_save_handler,但是没有一个实例表明您还必须设置register_shutdown_function('session_write_close')(引用)。

  • Several (older) guides refer to an outdated SQL Database structure, and should not be used. The database structure that you need for saving session data to the database is: id/access/data. That's it. no need for various extra timestamp columns as I've seen on a few "guides" and examples.

    一些(较老的)指南引用过时的SQL数据库结构,不应该使用。将会话数据保存到数据库所需的数据库结构是:id/access/data。就是这样。不需要各种额外的时间戳列,正如我在一些“指南”和示例中看到的那样。

    • Several of the older guides also have outdated MySQL syntax such as DELETE * FROM ...
    • 一些较老的指南也有过时的MySQL语法,比如从……
  • The class [made in my question] must implement the SessionHandlerInterface . I have seen guides (referenced above) that give the implementation of sessionHandler which is not a suitable interface. Perhaps previous versions of PHP had a slightly different method (probably <5.4).

    类(在我的问题中创建)必须实现SessionHandlerInterface。我已经看到了给sessionHandler的实现提供了一个不合适的接口的指南(上面提到过)。可能以前的PHP版本有一个稍微不同的方法(可能<5.4)。

  • The session class methods must return the values set out by the PHP manual. Again, probably inherited from pre-5.4 PHP but two guides I read stated that class->open returns the row to be read, whereas the PHP manual states that it needs to return true or false only.

    会话类方法必须返回PHP手册中设置的值。同样,可能继承自pre-5.4 PHP,但我读过的两个指南指出,class->open返回要读取的行,而PHP手册声明它只需要返回true或false。

  • This is the cause of my Original Issue: I was using custom session names (actually id's as session names and session id's are the same thing!) as per this very good * post and this was generating a session name that was 128 characters long. As the session name is the sole key that is needed to be cracked to compromise a session and take over with a session hijacking then a longer name/id is a very good thing.

    这就是我最初问题的原因:我使用自定义会话名称(实际上id作为会话名称,会话id是一样的!)由于会话名是惟一需要破解的密钥,因此需要使用较长的名称/id来破坏会话并接管会话。

    • But, this caused an issue because MySQL was silently slicing the session id down to just 32 characters instead of 128, so it was never able to find the session data in the database. This was a completely silent issue (maybe due to my database connection class not throwing warnings of such things). But this is the one to watch out for. If you have any issues with retrieving sessions from a database first check is that the full session id can be stored in the field provided.
    • 但是,这引起了一个问题,因为MySQL将会话id悄悄地分割为32个字符,而不是128个字符,因此无法在数据库中找到会话数据。这是一个完全无声的问题(可能是由于我的数据库连接类没有抛出此类问题的警告)。但这是值得注意的。如果您对从数据库检索会话有任何问题,那么首先要检查的是,完整的会话id可以存储在提供的字段中。

So with all that out of the way there are some extra details to add as well:

因此,除了这些之外,还有一些额外的细节需要补充:

The PHP manual page (linked above) shows an unsuitable pile of lines for a class object:

PHP手册页面(链接在上面)显示了一堆不适合类对象的行:

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

Whereas it works just as well if you put this in the class constructor:

然而,如果你把它放在类构造函数中,它也同样有效:

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();
    }
...
}

This means that to then start a session on your output page all you need is:

这意味着在您的输出页面上启动一个会话,您所需要的就是:

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

//Bang session has been setup and started and works

For reference the complete Session communication class is as follows, this works with PHP 5.6 (and probably 7 but not tested on 7 yet)

为了引用完整的会话通信类,如下所示,这适用于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();
    }

}

Usage: As shown just above the class code text.

用法:如类代码文本上方所示。