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

2024-05-08

我有两个网站,一个是 TLS,一个不是,两个都适用于同一个客户端,但我需要这些网站彼此(并且仅彼此)共享通用数据users, orders, accounts etc.

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

我四处挖掘发现这个有用的指南 http://culttt.com/2013/02/04/how-to-save-php-sessions-to-a-database/以及这个较旧的但是有用的指南 http://www.tonymarston.net/php-mysql/session-handler.html。我还发现本指南 http://www.codeproject.com/Articles/798978/Session-Storage-in-a-MySQL-Database其中 MySQL 稍微更新一些。

我编写了一个接口类,但它只能部分工作,它将会话数据存储在数据库中,但不检索它。我也用过PHP手册中建议的方法 http://php.net/manual/en/function.session-set-save-handler.php.

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;

请注意:在向您展示我的接口类之前,请知道数据库连接使用我自己定制的接口,并且其本身工作得很好。

The $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;

Issue:

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

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

为什么不起作用?


经过几个小时的调试,我发现在大量 Google 搜索中找到的参考文章以及 Stack Overflow 答案的一个重要子集,例如here https://stackoverflow.com/a/11272422/3536236, here https://stackoverflow.com/a/2950504/3536236 and here https://stackoverflow.com/a/30290579/3536236全部提供无效或过时的信息。

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

  • 虽然所有在线示例都表明您可以“填写”session_set_save_handler,他们都没有声明您还必须设置register_shutdown_function('session_write_close') too (参考 http://php.net/manual/en/function.session-set-save-handler.php).

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

    • 一些较旧的指南也有过时的 MySQL 语法,例如DELETE * FROM ...
  • [在我的问题中提出] 课程必须实施 http://php.net/manual/en/language.oop5.interfaces.php the SessionHandlerInterface。我看过指南(上面提到的),它给出了实施sessionHandler这不是一个合适的接口。也许以前版本的 PHP 有一个稍微不同的方法(可能

  • 会话类方法must返回 PHP 手册中规定的值。同样,可能是从 5.4 之前的 PHP 继承的,但我读过的两本指南指出class->open返回要读取的行,而PHP 手册说明 http://php.net/manual/en/function.session-set-save-handler.php它需要返回true or false only.

  • 这就是我最初问题的原因:我使用自定义会话名称(实际上 id 作为会话名称和会话 id是同一个东西!) 按照这篇非常好的 StackOverflow 帖子 https://stackoverflow.com/questions/8419332/proper-session-hijacking-prevention-in-php这会生成一个 128 个字符长的会话名称。由于会话名称是需要破解才能危害会话并接管的唯一密钥会话劫持 https://www.owasp.org/index.php/Session_hijacking_attack那么更长的名称/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();
    }
...
}

This意味着要在输出页面上启动会话,您需要做的是:

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

}

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐

  • Java RMI 通过互联网

    我正在用 Java 开发一个游戏 使用 RMI 进行所有网络通信 RMI 允许我调用服务器上的方法 但这对我来说还不够 我还希望服务器能够在连接的客户端之间传播消息 我的客户端查找服务器 它的接口扩展了远程 并在其上注册 它允许服务器知道谁
  • __declspec(align) 用于多个声明

    抱歉 这个非常简单的问题 无法在谷歌上找到答案 这个声明语法是 declspec align 16 float rF 4 declspec align 16 float gF 4 declspec align 16 float bF 4 相
  • 如何引用 models.py 之外的信号

    在文档中Django 它指定models py是定位信号回调函数的好地方 post save pre save etc 这段代码应该放在哪里 您可以将信号处理和注册代码放在您喜欢的任何地方 但是 您需要确保它所在的模块获取 尽早导入 以便信
  • 使用对象键作为 JSON 架构中的类型

    假设我想根据 Intellij IDEA 中的 JSON 架构验证 YAML 文件 该文件的结构如下 foo command touch tmp a txt I know I don t need this but it s an exam
  • 如何在 C# 中获取我当地时间的 UTC 等效时间

    我的机器处于 PDT 如果我说 DateTime Now 那么我将获得相当于 2012 年 9 月 18 日上午 6 00 00 的当地时间 我想获得此日期时间实例的 UTC 等效值 UTC 时间将比 PDT 早 7 小时 比 PST 早
  • Android - Firebase - 保存新数据而不覆盖旧数据

    创建此问题是因为我之前的问题包含 2 个问题 而不是将其缩小到 1 Aim 用户将能够存储新数据而不会覆盖之前提交的数据 描述 目前 当用户输入新报告时 事件报告节点中的用户事件报告数据将被覆盖 用户发送的旧事件报告中的数据应与新数据一起保
  • JAXB、Marshal 的问题 - 无法封送类型“java.lang.String”

    当我运行 marshal 操作时 出现以下错误 javax xml bind MarshalException with linked exception com sun istack internal SAXException2 unab
  • for 循环中列表项未更改

    当以下代码没有达到我预期的效果时 我感到震惊 lines list this is line 1 n this is line 2 n this is line 3 n for line in lines list line line st
  • Django 表单验证消息未显示

    我试图限制可以以表单上传的文件类型 大小和扩展名 该功能似乎有效 但未显示验证错误消息 我意识到if file size gt 4 1024 1024可能不是最好的方法 但我稍后会处理这个问题 这是 forms py class Produ
  • JavaScript:解决意外字符“#”

    介绍 我的 React 应用程序可以执行npm run build在开发模式下 webpack config dev js 但不在生产模式下 webpack config prod js 抛出以下错误 Module parse failed
  • java IO将一个文件复制到另一个文件

    我有两个 Java io File 对象 file1 和 file2 我想将 file1 的内容复制到 file2 有没有一种标准方法可以做到这一点 而无需我创建一个读取 file1 并写入 file2 的方法 不 没有内置方法可以做到这一
  • Azure PostgreSQL 时间点还原不起作用

    我们在 Azure 中有一个 Postgre 数据库 但遇到了一个问题 表中的所有行都被删除 我们尝试使用 de azure 门户中的 时间点还原 选项 但创建的数据库与当前运行的数据库具有相同的数据 我们还尝试了其他日期和时间 数据库问题
  • C++ 中的矩阵类

    我正在做一些线性代数数学 并且正在寻找一些真正轻量级且易于使用的矩阵类 可以处理不同的维度 基本上是 2x2 2x1 3x1 和 1x2 我认为此类可以使用模板来实现 并在某些情况下使用一些专门化来提高性能 有人知道任何可用的简单实现吗 我
  • 隐藏一个表单,切换到第二个表单,关闭第二个表单并取消隐藏第一个表单

    我已经查看了所有建议的答案 但似乎没有什么适合我正在寻找的内容 我想从主窗体中调用第二个窗体 在第二个窗体处于活动状态时隐藏主窗体 然后在第二个窗体关闭时取消隐藏主窗体 基本上我想在两种形式之间 切换 到目前为止我有 在我的主要形式中 pr
  • 优雅地终止 WCF 服务 - 完成所有打开的会话并限制新会话

    我有一个我编写的 WCF 服务 它托管在 Windows 服务中 它以 PerSession 模式运行 该服务允许客户端通过该服务远程打开文件 更改文件以及关闭文件 到目前为止一切工作都非常顺利 当 Windows 服务停止时 我希望能够让
  • 使用 None 值从 Python int 列表初始化 numpy 掩码数组

    正如问题的答案所示将具有 None 值的 python 列表转换为具有 nan 值的 numpy 数组 https stackoverflow com questions 19456239 convert python list with
  • 如何在Dialog中使用数据绑定?

    我在对话框中实现数据绑定时遇到问题 是否可以 下面是我的 xml
  • Angular UI Bootstrap Modal - 如何防止用户交互

    在我当前的用例中 我尝试使用 Angular ui 模态窗口来显示我们在后台进程中执行的计算进度 我们在完成后禁用该进程 一切运作良好 我只想禁止用户单击背景中的任何元素 知道我们该怎么做吗 打开模态窗口时 您可以传递以下选项 以防止用户关
  • 如何在服务器控件属性中使用 ASP.NET <%= 标记?

    这有效 span value span 这不起作用
  • 如何将 PHP 会话数据保存到数据库而不是文件系统中?

    我有两个网站 一个是 TLS 一个不是 两个都适用于同一个客户端 但我需要这些网站彼此 并且仅彼此 共享通用数据users orders accounts etc 这通常可以通过以下方式完成 SESSION数据 但我显然这些不能跨其他站点工