PHP Conference Japan 2024

SessionHandler 类

(PHP 5 >= 5.4.0, PHP 7, PHP 8)

简介

SessionHandler 是一个特殊的类,可以通过继承来公开当前的内部 PHP 会话保存处理程序。它有七个方法,分别包装了七个内部会话保存处理程序回调(openclosereadwritedestroygccreate_sid)。默认情况下,此类将包装由 session.save_handler 配置指令定义的任何内部保存处理程序,该指令通常默认为 files。其他内部会话保存处理程序由 PHP 扩展提供,例如 SQLite(作为 sqlite)、Memcache(作为 memcache)和 Memcached(作为 memcached)。

当使用 session_set_save_handler()SessionHandler 的普通实例设置为保存处理程序时,它将包装当前的保存处理程序。扩展自 SessionHandler 的类允许您覆盖方法或通过调用父类方法拦截或过滤它们,这些方法最终包装了内部 PHP 会话处理程序。

例如,这允许您拦截 readwrite 方法来加密/解密会话数据,然后将结果传递到父类和从父类传递。或者,可以选择完全覆盖像垃圾回收回调 gc 这样的方法。

因为 SessionHandler 包装了当前的内部保存处理程序方法,所以上述加密示例可以应用于任何内部保存处理程序,而无需了解处理程序的内部细节。

要使用此类,首先使用 session.save_handler 设置您希望公开的保存处理程序,然后将 SessionHandler 或其扩展类的实例传递给 session_set_save_handler()

请注意,此类的回调方法旨在由 PHP 在内部调用,而不是从用户空间代码调用。返回值同样由 PHP 在内部处理。有关会话工作流的更多信息,请参阅 session_set_save_handler()

类概要

class SessionHandler implements SessionHandlerInterface, SessionIdInterface {
/* 方法 */
public close(): bool
public create_sid(): string
public destroy(string $id): bool
public gc(int $max_lifetime): int|false
public open(string $path, string $name): bool
public read(string $id): string|false
public write(string $id, string $data): bool
}

备注

警告

此类旨在公开当前的内部 PHP 会话保存处理程序,如果您想编写自己的自定义保存处理程序,请实现 SessionHandlerInterface 接口,而不是扩展 SessionHandler

示例

示例 #1 使用 SessionHandler 向内部 PHP 保存处理程序添加加密。

<?php

/**
* 解密 AES 256
*
* @param data $edata
* @param string $password
* @return 解密后的数据
*/
function decrypt($edata, $password) {
$data = base64_decode($edata);
$salt = substr($data, 0, 16);
$ct = substr($data, 16);

$rounds = 3; // 取决于密钥长度
$data00 = $password.$salt;
$hash = array();
$hash[0] = hash('sha256', $data00, true);
$result = $hash[0];
for (
$i = 1; $i < $rounds; $i++) {
$hash[$i] = hash('sha256', $hash[$i - 1].$data00, true);
$result .= $hash[$i];
}
$key = substr($result, 0, 32);
$iv = substr($result, 32,16);

return
openssl_decrypt($ct, 'AES-256-CBC', $key, true, $iv);
}

/**
* 加密 AES 256
*
* @param data $data
* @param string $password
* @return base64 加密后的数据
*/
function encrypt($data, $password) {
// 使用 random_bytes() 生成密码学安全的随机盐
$salt = random_bytes(16);

$salted = '';
$dx = '';
// 对密钥(32)和初始化向量(16)进行加盐 = 48
while (strlen($salted) < 48) {
$dx = hash('sha256', $dx.$password.$salt, true);
$salted .= $dx;
}

$key = substr($salted, 0, 32);
$iv = substr($salted, 32,16);

$encrypted_data = openssl_encrypt($data, 'AES-256-CBC', $key, true, $iv);
return
base64_encode($salt . $encrypted_data);
}

class
EncryptedSessionHandler extends SessionHandler
{
private
$key;

public function
__construct($key)
{
$this->key = $key;
}

public function
read($id)
{
$data = parent::read($id);

if (!
$data) {
return
"";
} else {
return
decrypt($data, $this->key);
}
}

public function
write($id, $data)
{
$data = encrypt($data, $this->key);

return
parent::write($id, $data);
}
}

// 我们将拦截本机的“files”处理程序,但同样适用于其他内部本机处理程序,例如“sqlite”、“memcache”或“memcached”
// 这些由 PHP 扩展提供。
ini_set('session.save_handler', 'files');

$key = 'secret_string';
$handler = new EncryptedSessionHandler($key);
session_set_save_handler($handler, true);
session_start();

// 继续通过键从 $_SESSION 设置和检索值

注意:

由于此类的 methods 旨在由 PHP 在正常的会话工作流程中内部调用,因此对父 methods 的子类调用(即实际的内部本机处理程序)将返回 false,除非会话实际上已启动(自动或通过显式 session_start())。在编写单元测试时,需要考虑这一点,因为可能手动调用类的 methods。

目录

添加注释

用户贡献的注释 8 个注释

rasmus at mindplay dot dk
9 年前
由于会话处理程序的生命周期相当复杂,我发现仅用文字解释很难理解 - 所以我跟踪了对自定义 SessionHandler 的函数调用,并创建了此概述,准确地说明了当您调用各种会话 methods 时会发生什么

https://gist.github.com/mindplay-dk/623bdd50c1b4c0553cd3

我希望这能让您更容易地实现自定义 SessionHandler 并第一次就正确地实现它 :-)
tuncdan dot ozdemir dot peng at gmail dot com
1 年前
那些计划实现自己的 SessionHandler 的人,假设使用数据库系统,请确保您的“create_sid”方法使用创建的新会话 ID 在数据库中创建一条新记录,其中“data”为空字符串“”,否则当调用“read”方法时(无论是否是全新的会话,都会始终调用),您将收到错误,因为还没有使用该会话 ID 的记录。有趣的是,您收到的错误听起来像是 PHP 正在尝试在本地驱动器上打开会话(来自您的 .ini 文件)。
tony at marston-home dot demon dot co dot uk
6 年前
您的自定义会话处理程序不应包含对任何会话函数的调用,例如 session_name() 或 session_id(),因为相关值作为参数传递给各种处理程序方法。尝试从其他来源获取值可能无法按预期工作。
tuncdan dot ozdemir dot peng at gmail dot com
1 年前
设置自己的会话处理程序的最佳方法是扩展本机 SessionHandler(根据您的需要覆盖 7 个方法 + 构造函数,保持相同的签名)。或者,如果您计划使用“lazy_write”,还可以实现 SessionUpdateTimestampHandlerInterface。

选项 1

class MyOwnSessionHandler extends \SessionHandler { ..... }

选项 2

class MyOwnSessionHandler extends \SessionHandler implements \SessionUpdateTimestampHandlerInterface { ..... }

我不建议这样做

class MyOwnSessionHandler implements \SessionHandlerInterface, \SessionIdInterface, \SessionUpdateTimestampHandlerInterface { ... }

如果您好奇,以下是按顺序调用的方法(使用 PHP 8.2 和 Windows 11 64 位上的 XAMPP v3.3.0)

- open(始终调用)

- validateId 和/或 create_sid

如果您实现了 SessionUpdateTimestampHandlerInterface,则会调用 validateId。如果验证失败,则会调用 create_sid。

如果需要新的会话 ID(新会话等),则会调用 create_sid。

- read(始终调用)

- write 或 updateTimestamp 或 destroy

如果您调用“destroy”,则不会调用“write”或“updateTimestamp”。

如果您开启了“lazy_write”并且实现了 SessionUpdateTimestampHandlerInterface,
则如果没有任何更改,则会调用“updateTimestamp”而不是“write”。

- close(始终调用)
saccani dot francesco dot NOSPAM at gmail dot com
4 年前
我创建了这个 gist 以提供 PHP 会话处理程序生命周期(更新到 7.0 或更高版本)的完整概述。特别是,我想强调在使用本机 PHP 函数进行会话管理时,会调用哪些方法以及调用的顺序。

https://gist.github.com/franksacco/d6e943c41189f8ee306c182bf8f07654

我希望此分析将帮助所有有兴趣详细了解 PHP 执行的本机会话管理以及自定义会话处理程序应该执行什么操作的开发人员。
欢迎任何评论或建议。
RomanV
7 个月前
请注意,当您从 \SessionHandler 继承时,您会隐式禁用会话严格模式,因为它没有实现 SessionUpdateTimestampHandlerInterface(请参阅:https://php.net/manual/en/session.configuration.php#ini.session.use-strict-mode),因此不会调用 validateId()。

在每次更改任何权限级别(例如,在身份验证、密码/权限/用户角色更改期间)时,调用 session_regenerate_id() 函数至关重要,以减轻会话固定攻击!
jeremie dot legrand at komori-chambon dot fr
8 年前
这是一个包装器,用于在文件中记录每个会话的操作。有助于调查会话锁定(阻止 PHP 为同一客户端提供同时请求)。
只需更改末尾的文件名即可将日志转储到您想要的位置。

class DumpSessionHandler extends SessionHandler {
private $fich;

public function __construct($fich) {
$this->fich = $fich;
}

public function close() {
$this->log('close');
return parent::close();
}

public function create_sid() {
$this->log('create_sid');
return parent::create_sid();
}

public function destroy($session_id) {
$this->log('destroy('.$session_id.')');
return parent::destroy($session_id);
}

public function gc($maxlifetime) {
$this->log('close('.$maxlifetime.')');
return parent::gc($maxlifetime);
}

public function open($save_path, $session_name) {
$this->log('open('.$save_path.', '.$session_name.')');
return parent::open($save_path, $session_name);
}

public function read($session_id) {
$this->log('read('.$session_id.')');
return parent::read($session_id);
}

public function write($session_id, $session_data) {
$this->log('write('.$session_id.', '.$session_data.')');
return parent::write($session_id, $session_data);
}

private function log($action) {
$base_uri = explode('?', $_SERVER['REQUEST_URI'], 2)[0];
$hdl = fopen($this->fich, 'a');
fwrite($hdl, date('Y-m-d h:i:s').' '.$base_uri.' : '.$action."\n");
fclose($hdl);
}
}
ini_set('session.save_handler', 'files');
$handler = new DumpSessionHandler('/path/to/dump_sessions.log');
session_set_save_handler($handler, true);
wei dot kavin at gmail dot com
5 年前
php -S localhost:8000 -t foo/

touch index.php

vi index.php
============================================================
class NativeSessionHandler extends \SessionHandler
{
public function __construct($savePath = null)
{
if (null === $savePath) {
$savePath = ini_get('session.save_path');
}

$baseDir = $savePath;

if ($count = substr_count($savePath, ';')) {
if ($count > 2) {
throw new \InvalidArgumentException(sprintf('Invalid argument $savePath \'%s\'', $savePath));
}

// characters after last ';' are the path
$baseDir = ltrim(strrchr($savePath, ';'), ';');
}

if ($baseDir && !is_dir($baseDir) && !@mkdir($baseDir, 0777, true) && !is_dir($baseDir)) {
throw new \RuntimeException(sprintf('Session Storage was not able to create directory "%s"', $baseDir));
}

ini_set('session.save_path', $savePath);
ini_set('session.save_handler', 'files');
}
}

$handler = new NativeSessionHandler("/var/www/foo");
session_set_save_handler($handler, true);
session_start();
$a = $handler->write("aaa","bbbb");var_dump($a);exit;

============================================================

输出:bool(false)
To Top