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

Posted

技术标签:

【中文标题】如何将 PHP 会话数据保存到数据库而不是文件系统中?【英文标题】:How do I save PHP session data to a database instead of in the file system? 【发布时间】:2016-08-13 16:55:24 【问题描述】:

我有两个网站,一个是 TLS,一个不是,两者都用于同一个客户端,但我需要这些网站相互(并且仅相互)共享 用户 的公共数据, 订单账户

这通常使用$_SESSION 数据来完成,但我显然无法在其他站点上使用这些数据,而且我发现我可以将会话数据存储在数据库 (mysql) 中而不是文件系统中。

我已经挖掘并找到了This useful guide 以及这个较旧但 useful guide。我还发现了this guide,它的 MySQL 稍微更新一些。

我写了一个接口类,但它只是部分工作,它将会话数据存储在数据库中,但它不检索它。我也用过suggested method from the 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 连接使用我自己定制的接口,并且它本身就可以完美运行。

$sessionDBconnectionUrl 包含会话数据库连接详细信息,因为我将会话保存在与主要网站内容不同的数据库中。

我的界面类(基于以上所有链接)

<?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 错误。

为什么它不起作用?

【问题讨论】:

我猜你最糟糕的问题是域之间的 cookie 共享(是的,客户端)...... 为什么?你能再解释一下吗? 这两个站点将使用一个转移页面,因此从主站点对购物网站的所有访问都将通过一个转移页面,该页面在将用户传递到他们的目标页面之前设置会话数据转移。 请看,会话密钥存储在浏览器上的 cookie 中,因此站点知道它是同一个访问者(cookie 在每个请求中发送),但是它们被域沙盒化。如果http://mywebsite.com/ 尝试访问您的http://google.com/ cookie,这是被禁止的并且存在安全漏洞(有人可能会窃取您的 gmail 会话)。 这就是为什么这没有发生,两个站点都引用存储在数据库中的会话。因此,通过从一个站点传递到另一个站点的识别标记,所有数据都可以在 new 会话 cookie 中检索。 【参考方案1】:

我在几个小时的调试过程中发现,在众多 Google 搜索中找到的参考文章以及 Stack Overflow 答案的重要子集(例如 here、here 和 here)都提供无效或过时信息。

在将会话数据保存到数据库时可能会导致 [严重] 问题:

虽然所有在线示例都表明您可以“填充”session_set_save_handler,但没有一个说明您也必须设置 register_shutdown_function('session_write_close') (reference)。

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

一些较旧的指南也有过时的 MySQL 语法,例如 DELETE * FROM ...

[在我的问题中提出的]类必须 implement SessionHandlerInterface 。我已经看到指南(上面引用过)给出了 sessionHandler 的实现,这不是一个合适的接口。也许以前版本的 PHP 有一个稍微不同的方法(可能

会话类方法必须返回 PHP 手册中规定的值。同样,可能继承自 5.4 之前的 PHP,但我读过的两个指南指出 class-&gt;open 返回要读取的行,而 PHP manual states 只需要返回 truefalse

这是我原来的问题的原因:我使用自定义会话名称(实际上 id 作为会话名称和会话 id 的 是同一件事!)按照 @ 987654327@ 这会生成一个 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

完整的 Session 通信类参考如下,它适用于 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();
    


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

【讨论】:

仅供参考:open() 不使用 $savepath 参数 会话最初添加到数据库的时间是什么时候?您的open 函数实际上应该只检查是否存在与数据库的有效连接。现在它似乎正在检查数据库中是否已经存在有效会话 - 如果这是一个正在创建的新会话,我怀疑它是否会返回 true @paul 看看原来的接口实现: public function open($savePath, $sessionName) $this->savePath = $savePath; if (!is_dir($this->savePath)) mkdir($this->savePath, 0777); 返回真; @Martin,第二页上的会话变量设置为空。我在一个页面中设置会话值并且这些值存储到数据库中,但是当我在另一个页面上检索它时它正在将我注销并将会话变量设置为一个空数组,你知道为什么吗? @ZOLDIK open 类只是告诉 PHP 核心它可以继续尝试从会话中读取/写入,因为已找到有效会话,这就是它返回布尔值的原因会话数据。

以上是关于如何将 PHP 会话数据保存到数据库而不是文件系统中?的主要内容,如果未能解决你的问题,请参考以下文章

如何每次将新数据保存到我的 plist 文件而不是替换旧数据?

如何使用 javascript 而不是 Django 会话将表单数据(Django 表单)存储到浏览器的本地存储?

php 魔术师调整大小 - 显示图像而不是保存到文件夹

如何存储用户的会话数据

PHP-会话控制-Session

PHP 不会打开 fifo 进行写入