session_set_save_handler

(PHP 4, PHP 5, PHP 7, PHP 8)

session_set_save_handler设置用户级会话存储函数

描述

session_set_save_handler(
    可调用 $open,
    可调用 $close,
    可调用 $read,
    可调用 $write,
    可调用 $destroy,
    可调用 $gc,
    可调用 $create_sid = ?,
    可调用 $validate_sid = ?,
    可调用 $update_timestamp = ?
): 布尔值

可以注册以下原型

session_set_save_handler(对象 $sessionhandler, 布尔值 $register_shutdown = true): 布尔值

session_set_save_handler() 设置用户级会话存储函数,用于存储和检索与会话关联的数据。当需要使用其他存储方法而不是 PHP 会话提供的存储方法时,这非常有用,例如将会话数据存储在本地数据库中。

参数

此函数有两个原型。

sessionhandler

实现 SessionHandlerInterface 的类的实例,以及可选的 SessionIdInterface 和/或 SessionUpdateTimestampHandlerInterface,例如 SessionHandler,以注册为会话处理程序。

register_shutdown

注册 session_write_close() 作为 register_shutdown_function() 函数。

或者
open

具有以下签名的可调用对象

open(字符串 $savePath, 字符串 $sessionName): 布尔值

open 回调类似于类中的构造函数,在会话打开时执行。当会话自动启动或使用 session_start() 手动启动时,它是第一个执行的回调函数。返回值为 true 表示成功,false 表示失败。

close

具有以下签名的可调用对象

close(): 布尔值

close 回调类似于类中的析构函数,在会话写入回调被调用后执行。当调用 session_write_close() 时也会调用它。返回值应为 true 表示成功,false 表示失败。

read

具有以下签名的可调用对象

read(字符串 $sessionId): 字符串

read 回调必须始终返回一个会话编码(序列化)字符串,或者如果没有任何数据要读取,则返回一个空字符串。

当会话启动或调用 session_start() 时,PHP 内部会调用此回调函数。在调用此回调函数之前,PHP 会调用 open 回调函数。

此回调函数返回的值必须与最初传递给 write 回调函数进行存储的序列化格式完全相同。PHP 会自动对返回值进行反序列化,并用于填充 $_SESSION 超级全局变量。虽然数据看起来类似于 serialize(),但请注意,它是一种不同的格式,在 session.serialize_handler ini 设置中指定。

write

具有以下签名的可调用对象

write(字符串 $sessionId, 字符串 $data): 布尔值

当需要保存和关闭会话时,会调用 write 回调函数。此回调函数接收当前会话 ID 和 $_SESSION 超级全局变量的序列化版本。PHP 内部使用的序列化方法在 session.serialize_handler ini 设置中指定。

传递给此回调函数的序列化会话数据应根据传递的会话 ID 进行存储。检索此数据时,read 回调函数必须返回最初传递给 write 回调函数的精确值。

当 PHP 关闭或显式调用 session_write_close() 时,会调用此回调函数。请注意,在执行此函数后,PHP 会在内部执行 close 回调函数。

注意:

"write" 处理程序在输出流关闭后才执行。因此,"write" 处理程序中调试语句的输出永远不会在浏览器中显示。如果需要调试输出,建议将调试输出写入文件。

destroy

具有以下签名的可调用对象

destroy(字符串 $sessionId): 布尔值

当使用 session_destroy() 或使用 session_regenerate_id()(将 destroy 参数设置为 true)销毁会话时,会执行此回调函数。返回值应为 true 表示成功,false 表示失败。

gc

具有以下签名的可调用对象

gc(整数 $lifetime): 布尔值

垃圾回收回调函数会由 PHP 定期调用,以清除旧的会话数据。频率由 session.gc_probabilitysession.gc_divisor 控制。传递给此回调函数的寿命值可以在 session.gc_maxlifetime 中设置。返回值应为 true 表示成功,false 表示失败。

create_sid

具有以下签名的可调用对象

create_sid(): 字符串

当需要新的会话 ID 时,会执行此回调函数。没有提供任何参数,返回值应为对您的处理程序有效的会话 ID 字符串。

validate_sid

具有以下签名的可调用对象

validate_sid(字符串 $key): 布尔值

当要启动会话、提供会话 ID 且 session.use_strict_mode 已启用时,会执行此回调函数。key 是要验证的会话 ID。如果已存在具有该 ID 的会话,则会话 ID 有效。返回值应为 true 表示成功,false 表示失败。

update_timestamp

具有以下签名的可调用对象

update_timestamp(字符串 $key, 字符串 $val): 布尔值

当会话更新时执行此回调。 key 是会话 ID,val 是会话数据。 返回值应为 **true** 表示成功,**false** 表示失败。

返回值

成功时返回 **true**,失败时返回 **false**。

示例

示例 #1 自定义会话处理程序:请参阅 SessionHandlerInterface 概要中的完整代码。

我们只在此处显示调用方式,完整的示例可以在上面链接的 SessionHandlerInterface 概要中看到。

请注意,我们使用 OOP 原型与 session_set_save_handler(),并使用函数的参数标志注册关闭函数。 这在将对象注册为会话保存处理程序时通常建议使用。

<?php
class MySessionHandler implements SessionHandlerInterface
{
// 在此处实现接口
}

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

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

注意

警告

在对象销毁后调用 writeclose 处理程序,因此无法使用对象或抛出异常。 无法捕获异常,因为不会捕获异常,也不会显示任何异常跟踪,执行将突然停止。 但是,对象析构函数可以使用会话。

可以从析构函数中调用 session_write_close() 来解决此循环依赖问题,但最可靠的方法是如上所述注册关闭函数。

警告

如果在脚本终止时关闭会话,则当前工作目录会随着某些 SAPI 而更改。 可以使用 session_write_close() 更早地关闭会话。

另请参阅

添加备注

用户贡献的备注 41 备注

andreipa at gmail dot com
8 年前
在花了大量时间了解 PHP 会话如何与数据库一起使用以及尝试使之正常运行但失败之后,我决定重写我们朋友 Stalker 的版本。

//数据库
CREATE TABLE `Session` (
`Session_Id` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`Session_Expires` datetime NOT NULL,
`Session_Data` text COLLATE utf8_unicode_ci,
PRIMARY KEY (`Session_Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
SELECT * FROM mydatabase.Session;

<?php
//inc.session.php

class SysSession implements SessionHandlerInterface
{
private
$link;

public function
open($savePath, $sessionName)
{
$link = mysqli_connect("server","user","pwd","mydatabase");
if(
$link){
$this->link = $link;
return
true;
}else{
return
false;
}
}
public function
close()
{
mysqli_close($this->link);
return
true;
}
public function
read($id)
{
$result = mysqli_query($this->link,"SELECT Session_Data FROM Session WHERE Session_Id = '".$id."' AND Session_Expires > '".date('Y-m-d H:i:s')."'");
if(
$row = mysqli_fetch_assoc($result)){
return
$row['Session_Data'];
}else{
return
"";
}
}
public function
write($id, $data)
{
$DateTime = date('Y-m-d H:i:s');
$NewDateTime = date('Y-m-d H:i:s',strtotime($DateTime.' + 1 hour'));
$result = mysqli_query($this->link,"REPLACE INTO Session SET Session_Id = '".$id."', Session_Expires = '".$NewDateTime."', Session_Data = '".$data."'");
if(
$result){
return
true;
}else{
return
false;
}
}
public function
destroy($id)
{
$result = mysqli_query($this->link,"DELETE FROM Session WHERE Session_Id ='".$id."'");
if(
$result){
return
true;
}else{
return
false;
}
}
public function
gc($maxlifetime)
{
$result = mysqli_query($this->link,"DELETE FROM Session WHERE ((UNIX_TIMESTAMP(Session_Expires) + ".$maxlifetime.") < ".$maxlifetime.")");
if(
$result){
return
true;
}else{
return
false;
}
}
}
$handler = new SysSession();
session_set_save_handler($handler, true);
?>

<?php
//第 1 页
require_once('inc.session.php');

session_start();

$_SESSION['var1'] = "我的葡萄牙语文本:SOU Gaucho!";
?>

<?php
//第 2 页
require_once('inc.session.php');

session_start();

if(isset(
$_SESSION['var1']){
echo
$_SESSION['var1'];
}
//输出:我的葡萄牙语文本:SOU Gaucho!
?>
ohcc at 163 dot com
6 年前
未记录的是,自 PHP 7.0 起,支持可调用对象 $validate_sid 和 $update_timestamp 用于
"bool session_set_save_handler ( callable $open , callable $close , callable $read , callable $write , callable $destroy , callable $gc [, callable $create_sid [, callable $validate_sid [, callable $update_timestamp ]]] )" 的原型。

validate_sid($sessionId)
此回调用于验证 $sessionId。 对于有效的会话 ID $sessionId,其返回值应为 true;对于无效的会话 ID $sessionId,其返回值应为 false。 如果返回 false,则会生成一个新的会话 ID 来替换无效的会话 ID $sessionId。

update_timestamp($sessionId)
此回调用于更新时间戳,其返回值应为 true 表示成功,false 表示失败。

如果使用此原型,如果提供的参数少于 6 个或多于 session_set_save_handler() 接受的参数,则会收到 "Wrong parameter count for session_set_save_handler()" 警告。

如果使用 session_set_save_handler(SessionHandlerInterface $sessionhandler [, bool $register_shutdown = true ]) 的 OOP 原型,即使在 PHP 7.2 中,也不会调用名为 validate_sid 或 update_timestamp 的 $sessionhandler 类成员方法,但从 PHP 5.5.1 起,支持名为 create_sid 的成员方法。

今天是 2017 年 12 月 16 日,即使 PHP 文档也可能会在之后某个时间进行更新。
peter at brandrock dot co dot za
6 年前
如果保存到数据库,如本页的示例所示,为了性能,请考虑以下几点。

构建 Sessions 表,并在 SessionExpires 列上建立索引,以便快速识别在垃圾回收阶段要删除的行。

最好在每次会话启动/打开时进行垃圾回收 "delete from sessions where expiresOn < $now"。如果您在过期时间上建立了索引,这不会造成很大影响,并且可以将负载均匀分配到所有用户。如果可能出现大量会话同时过期,请包含 "limit 100" 子句,该子句设置为合理的值,以便每个用户共享负载。

使用 varchar 而不是 Text 来存储数据,因为 Text 将在页面外存储列,并且检索速度稍慢。只有当您的应用程序确实在会话中存储大量文本时才使用 Text。
ohcc at 163 dot com
6 年前
从 PHP 7.0 开始,您可以实现 SessionUpdateTimestampHandlerInterface 来
在 session_set_save_handler() 的非 OOP 原型中定义自己的会话 ID 验证方法(如 validate_sid)和时间戳更新方法(如 update_timestamp)。

SessionUpdateTimestampHandlerInterface 是 PHP 7.0 中引入的一个新接口,尚未进行文档记录。它有两个抽象方法:SessionUpdateTimestampHandlerInterface :: validateId($sessionId) 和 SessionUpdateTimestampHandlerInterface :: updateTimestamp($sessionId, $sessionData)。

<?php
/*
@author Wu Xiancheng
由于 SessionUpdateTimestampHandlerInterface 是在 PHP 7.0 中引入的,因此此代码结构仅适用于 PHP 7.0 及更高版本
使用此类,您可以验证 PHP 会话 ID 并使用 PHP 7.0 及更高版本中 session_set_save_handler() 的 OOP 原型更新 PHP 会话数据的时间戳
*/
class PHPSessionXHandler implements SessionHandlerInterface, SessionUpdateTimestampHandlerInterface {
public function
close(){
// 返回值应为 true 表示成功,或为 false 表示失败
// ...
}
public function
destroy($sessionId){
// 返回值应为 true 表示成功,或为 false 表示失败
// ...
}
public function
gc($maximumLifetime){
// 返回值应为 true 表示成功,或为 false 表示失败
// ...
}
public function
open($sessionSavePath, $sessionName){
// 返回值应为 true 表示成功,或为 false 表示失败
// ...
}
public function
read($sessionId){
// 返回值应为会话数据或空字符串
// ...
}
public function
write($sessionId, $sessionData){
// 返回值应为 true 表示成功,或为 false 表示失败
// ...
}
public function
create_sid(){
// 从 PHP 5.5.1 开始可用
// 当需要新的会话 ID 时,会在内部调用
// 无需参数,返回值应为创建的新会话 ID
// ...
}
public function
validateId($sessionId){
// 实现 SessionUpdateTimestampHandlerInterface::validateId()
// 从 PHP 7.0 开始可用
// 如果会话 ID 有效,返回值应为 true,否则为 false
// 如果返回 false,PHP 将在内部生成新的会话 ID
// ...
}
public function
updateTimestamp($sessionId, $sessionData){
// 实现 SessionUpdateTimestampHandlerInterface::validateId()
// 从 PHP 7.0 开始可用
// 返回值应为 true 表示成功,或为 false 表示失败
// ...
}
}
?>
Steven George
10 年前
请注意,除了在调用 write() 和 close() 之前销毁对象之外,PHP 似乎还会销毁类。也就是说,您甚至无法在 write() 和 close() 处理程序中调用外部类的静态方法 - PHP 将发出一个致命错误,说明 "类 xxxx 未找到"。
tomas at slax dot org
16 年前
关于 SAPIs:函数描述中提到的警告(某些 SAPIs 会更改当前工作目录)非常重要。

这意味着,如果您的回调 'write' 函数需要写入当前目录中的文件,它将无法找到该文件。您必须使用绝对路径,而不是依赖于当前工作目录。

我以为此警告仅适用于某些奇怪的环境,例如 Windows,但它确实发生在 Linux + Apache 2.2 + PHP 5 上。
polygon dot co dot in at gmail dot com
2 年前
以下是一个演示,用于检查会话函数执行的顺序。

<?php

ini_set
('session.use_strict_mode',true);

function
sess_open($sess_path, $sess_name) {
echo
'<br/>sess_open';
return
true;
}

function
sess_close() {
echo
'<br/>sess_close';
return
true;
}

function
sess_read($sess_id) {
echo
'<br/>sess_read';
return
'';
}

function
sess_write($sess_id, $data) {
echo
'<br/>sess_write';
return
true;
}

function
sess_destroy($sess_id) {
echo
'<br/>sess_destroy';
return
true;
}

function
sess_gc($sess_maxlifetime) {
echo
'<br/>sess_gc';
return
true;
}

function
sess_create_sid() {
echo
'<br/>sess_create_sid';
return
'RNS'.rand(0,10);
}

function
sess_validate_sid($sess_id) {
echo
'<br/>sess_validate_sid';
return
true;
}

function
sess_update_timestamp($sess_id,$data) {
echo
'<br/>sess_update_timestamp';
return
true;
}

session_set_save_handler(
'sess_open',
'sess_close',
'sess_read',
'sess_write',
'sess_destroy',
'sess_gc',
'sess_create_sid',
'sess_validate_sid',
'sess_update_timestamp'
);

session_start();

echo
'<br/>code here...';

?>

首次执行上述代码时的输出如下。
sess_open
sess_create_sid
sess_read
code here...
sess_write
sess_close

下一次执行的输出如下。
sess_open
sess_validate_sid
sess_read
code here...
sess_write
sess_close
tony at marston-home dot demon dot co dot uk
6 年前
您的自定义会话处理程序不应包含对任何会话函数(例如 session_name() 或 session_id())的调用,因为相关值作为参数传递给各种处理程序方法。尝试从其他来源获取值可能会产生预期之外的结果。
centurianii at yahoo dot co dot uk
6 年前
补充来自 andreipa at gmail dot com 的非常有用的类

1. 您应该从 SessionHandlerInterface 方法处理会话过期和数据 I/O。
2. 您不应从这些方法处理会话再生和数据修改,而应从静态方法处理,例如 Session::start()。
3. PHP 提供了很多示例,但没有说明应该从哪个角度工作。

此类的一个框架
namespace xyz;
class Session implements \SessionHandlerInterface, Singleton {
/** @var SessionToken $token 此命令的 SessionToken;
这是我的编程方法的一部分 */
protected $token;
/** @var PDO $dbh 数据库的 PDO 处理程序 */
protected $dbh;
/** @var $savePath 会话存储的位置 */
protected $savePath;
/** @var $type 会话类型 (['files'|'sqlite']) */
protected $type;
/** @var self $instance 此类的实例 */
static private $instance = null;

private function __construct() { ... }
static public function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function open($savePath, $sessionName) { ... }
public function close() {
if ($this->type == static::FILES) {
return true;
} elseif ($this->type == static::SQLITE) {
return true;
}
}
public function read($id) { ... }
public function write($id, $data) { ... }
public function destroy($id) { ... }
public function gc($maxlifetime) { ... }
static public function get($key) {
return (isset($_SESSION[$key]))? $_SESSION[$key] : null;
}
static public function set($key, $value) {
return $_SESSION[$key] = $value;
}
static public function newId() {...}
static public function start($call = null, $log = false) {
//1. 启动会话(发送第一个头文件)
if (session_status() != PHP_SESSION_ACTIVE) {
session_start(); //调用:open()->read()
}

//2. $_SESSION['session']: 会话控制数据数组
// 现有会话
if (is_array(static::get('session'))) {
$session = static::get('session');
// 新会话
} else {
$session = array();
}

$tmp = $_SESSION;
//对 $session 数组执行某些操作...
static::set('session', $session);
session_write_close(); //调用:write()->read()->close()
//在 if...else... 中创建新会话
session_id(static::newId());
session_start(); //调用:open()->read()
//如果要复制以前会话的数据
//$_SESSION = $tmp;
//对 $session 数组执行其他操作,并将它保存到新会话...
static::set('session', $session);

//6. 调用回调函数(仅对有效/新会话)
if ($call)
$call();
session_write_close(); //调用:write()->read()->close()
}
/**
* 定义自定义会话处理程序。
*/
static public function setHandler() {
// 提交自动会话
if (ini_get('session.auto_start') == 1) {
session_write_close();
}
$handler = static::getInstance();
session_set_save_handler($handler, true);
}
}

让我们启动一个会话
Session::setHandler();
Session::start();

我花了几个小时调试错误,发现第 3 个 Session::read() 结束时使用了一个空 Session::dbh,直到我意识到 Session::close() 不应该销毁该类的属性!
另外,我避免使用 session_create_id(),因为它只适用于 PHP 7 >= 7.1.0,我用静态 Session::newId() 代替。
shanikawm at gmail dot com
8 年前
这是一个使用 Oracle 表处理会话的类。
https://github.com/shanikawm/PHP_Oracle_Based_Session_Handler_Class

<?php
/**
* 作者:Shanika Amarasoma
* 日期:2016 年 6 月 24 日
* 使用 Oracle 数据库的 PHP 会话处理程序
* Oracle 创建表语句
CREATE TABLE PHP_SESSIONS
(
SESSION_ID VARCHAR2(256 BYTE) UNIQUE,
DATA CLOB,
TOUCHED NUMBER(38)
);
*/
class session_handler implements SessionHandlerInterface
{
private
$con;
public function
__construct() {
if(!
$this->con=oci_pconnect(DBUSER,DBPASS,CONNECTION_STR)){
die(
'数据库连接失败!');
}
}
public function
open($save_path ,$name){
return
true;
}
public function
close(){
return
true;
}
public function
read($session_id){
$query = "SELECT \"DATA\" FROM PHP_SESSIONS WHERE SESSION_ID=Q'{" . $session_id . "}'";
$stid = oci_parse($this->con, $query);
oci_execute($stid, OCI_DEFAULT);
$row = oci_fetch_array($stid, OCI_ASSOC + OCI_RETURN_LOBS);
oci_free_statement($stid);
return
$row['DATA'];
}
public function
write($session_id,$session_data){
$dquery="DELETE FROM PHP_SESSIONS WHERE SESSION_ID=Q'{".$session_id."}'";
$dstid = oci_parse($this->con,$dquery);
oci_execute($dstid, OCI_NO_AUTO_COMMIT);
oci_free_statement($dstid);
$query="INSERT INTO PHP_SESSIONS(SESSION_ID,TOUCHED,\"DATA\") VALUES(Q'{".$session_id."}',".time().",EMPTY_CLOB()) RETURNING \"DATA\" INTO :clob";
$stid = oci_parse($this->con,$query);
$clob=oci_new_descriptor($this->con,OCI_D_LOB);
oci_bind_by_name($stid, ':clob', $clob, -1, OCI_B_CLOB);
if(!
oci_execute($stid, OCI_NO_AUTO_COMMIT)){
@
oci_free_statement($stid);
return
false;
}
if(
$clob->save($session_data)){
oci_commit($this->con);
$return=true;
} else {
oci_rollback($this->con);
$return=false;
}
$clob->free();
oci_free_statement($stid);
return
$return;
}
public function
destroy($session_id){
$query="DELETE FROM PHP_SESSIONS WHERE SESSION_ID=Q'{".$session_id."}'";
$stid = oci_parse($this->con,$query);
oci_execute($stid, OCI_DEFAULT);
$rows=oci_num_rows($stid);
oci_commit($this->con);
oci_free_statement($stid);
if(
$rows>0){
return
true;
} else {
return
false;
}
}
public function
gc($maxlifetime){
$query="DELETE FROM PHP_SESSIONS WHERE TOUCHED<".(time()-$maxlifetime);
$stid = oci_parse($this->con,$query);
oci_execute($stid, OCI_DEFAULT);
$rows=oci_num_rows($stid);
oci_commit($this->con);
oci_free_statement($stid);
if(
$rows>0){
return
true;
} else {
return
false;
}
}
}
session_set_save_handler(new session_handler(), true);
session_start();
korvus at kgstudios dot net
19 年前
似乎当你调用 'session_name()' 时,php 会自动从 GET 中加载会话 ID(如果索引存在)并正确地将其传递给 'read' 回调方法,但 'write' 回调会调用两次:第一次是自动生成的会话 ID,然后是自定义会话 ID。

所以要注意你在回调内部执行的查询... 我疯了,因为我使用了 MySQL 'REPLACE' 语句来加速,我花了几个小时试图弄清楚为什么有 2 行而不是 1 行被影响(第一个 ID 正在插入,第二个正在更新)。

希望这有帮助!
ivo at magstudio dot net
21 年前
简单地说一下使用 session_set_save_handler() 时的麻烦。似乎 PHP 在内部按以下顺序调用会话管理函数:open()、read()、write()、close()。即使你没有调用 sesison_start(),close() 函数也会被调用,可能是出于清理等原因。
如果你尝试重新定义这些函数并调用 sessions_set_save_handler(),但有些事情不起作用(在我的情况下,会话数据没有被写入),最好按照它们被调用的顺序调试它们。它们不会向浏览器输出错误,但你可以使用 print 或 echo。
简而言之,如果你的 write() 函数没有按预期工作,请查看之前函数 - open() 和 read() 中的错误。
希望这能帮助人们节省几个小时的调试时间。
oliver at teqneers dot de
19 年前
对于一些人来说,了解这一点可能很重要,如果标准会话处理程序被 session_set_save_handler 覆盖,那么任何锁定都将不再起作用(在 session_read 和 session_write 之间)。可能会发生以下情况

脚本 "A" 启动。
读取会话数据。
. 脚本 "B" 启动
. 读取会话数据
运行 (30 秒) 添加会话数据
. 写入会话数据
. 脚本 "B" 停止
写入会话数据。
脚本 "A" 停止。

如果脚本 "A" 运行时间很长(比如 30 秒),同一个用户可能会启动另一个脚本 "B",它也使用会话。脚本 "B" 将启动并读取会话数据,即使脚本 "A" 仍在运行。因为脚本 "B" 速度更快,它将在脚本 "A" 结束之前完成其工作并写回其会话数据。现在脚本 "A" 结束并覆盖了脚本 "B" 的所有会话数据。如果你没有使用 session_set_save_handler,这种情况不会发生,因为在这种情况下,PHP 不会在脚本 "A" 结束之前启动脚本 "B"。
e dot sand at elisand dot com
15 年前
会话数据中的 "二进制" 数据似乎围绕着类/对象名称,如果你将会话数据通过一个函数传递来对其进行清理以防止 SQL 注入,你确实可能会遇到问题。

例如,使用 PDO::quote() 函数准备要注入的数据(在我的情况下,用于 SQLite3),它在遇到第一个二进制数据时就停止了,导致我的会话信息被破坏。

这个改变 *必须* 发生在 5.2 系列中的某个地方,因为我最近才在一个代码库中开始遇到这个问题,而该代码库已经在 PHP 5.2 的早期版本中经过测试并工作。

这实际上可能是一个 bug - 我还没有检查... 但要注意,也许使用 base64 对你的会话数据进行编码/解码是一个好主意,这样可以确保万无一失(尽管你现在的确无法在存储级别上直观地查看序列化后的会话信息,这对会话的动态调试来说是一个很大的问题)。
james at dunmore dot me dot uk
16 年前
我认为这里很重要的一点是,WRITE 方法应该使用 UPDATE+INSERT(或 MySQL 特定的 REPLACE)。

有一些示例代码“在那里”仅使用 UPDATE 作为写入方法,在这种情况下,当调用 session_regenerate_id 时,会话数据会丢失(因为更新会失败,因为键已更改)。

我刚刚浪费了一整天的时间才发现这个问题(我知道我应该事先考虑好 / 阅读文档,但这确实是一个很容易掉进去的陷阱)。
frank at interactinet dot com
13 年前
我在提交会话数据时遇到了麻烦。
要在不关闭会话的情况下“提交并继续”,请将以下代码放在“写入”方法的顶部

<?php

$id
= session_id();
session_write_close();
session_id($id);
session_start();

?>

请注意,PHP 每次生成新的会话 ID 时,都不会自动在数据库中更新。这可能会有用

<?php

public function resetSessionId()
{
$old = session_id();
session_regenerate_id();
$new = session_id();
SessionHandler::regenerate_id($old,$new);
}

public function
regenerate_id($old,$new)
{
$db = mysqli->connect(...);

$db->query('UPDATE sessions SET session_id = \''.$db->escape_string($new).'\'
WHERE session_id = \''
.$db->escape_string($old).'\'');
}
?>
carlos dot vini at gmail dot com
8 年前
如果您注册了自定义处理程序,ini_get('session.save_handler') 将返回 'user' 而不是 'file'
matt at openflows dot org
17 年前
请注意,出于安全原因,Debian 和 Ubuntu 发行版的 PHP 不会调用 _gc 来删除旧的会话,而是运行 /etc/cron.d/php*,它会检查 php.ini 中 session.gc_maxlifetime 的值并删除 /var/lib/php* 中的会话文件。这很好,但这意味着如果您编写了自己的会话处理程序,您需要显式调用自己的 _gc 函数。一个好的做法是在您的 _close 函数中执行此操作,如下所示

<?php
function _close() {
_gc(get_cfg_var("session.gc_maxlifetime"));
// 函数的其余部分在此处
}
?>
dummynick at gmail dot com
14 年前
我遇到了致命错误:没有堆栈帧的异常被抛出,我花了几天时间才找出原因。我使用 Memcache 存储会话,在我的自定义类中,我在写入方法中使用 Memcache 类。

我把写入方法中的代码放在 try-catch 块中,问题就解决了。
joel the usual at sign then purerave.com
14 年前
在将会话存储在数据库中时,通常使用现有的自定义数据库对象是有益的,但这会给最新版本的 PHP 5.3.1 带来问题。这在 PHP 5.2.x(Linux 和 Windows)上曾经可以正常工作。

现在的问题是,当执行结束时,session_write_close() 不会被自动调用,而是在所有对象都被销毁后调用,包括数据库对象!

有两种解决方法,要么在脚本的末尾手动调用 session_write_close(),要么不使用数据库对象。

我相信这是从一开始就预期的行为。
yangqingrong at gmail dot com
15 年前
session_set_save_handler 在 session_start 之前使用。如果您的会话被设置为自动启动,它将返回 FALSE 值。因此,您需要在 session_set_save_handler 之前添加 session_write_close() 来取消会话的自动启动。它就像这样

<?php
/*
qq:290359552
*/
session_write_close(); // 取消会话的自动启动,很重要

function open()
{
...
}
....
session_set_save_handler( ... );
session_start();
?>
bart2yk at yahoo dot com
14 年前
您可以在数据库对象析构函数中调用 session_write,以确保您仍然可以连接到 mysql 并且会话已写入。
sneakyimp AT hotmail DOT com
18 年前
这些函数的行为、返回值和确切的调用时间在此处记录得非常糟糕。我认为大家可能想知道

1) 调用 session_start() 会触发 PHP 首先调用您的 open 函数,然后调用您的 read 函数,然后再继续执行 session_start() 调用后的代码。

2) 在您的 open 函数中调用 session_id('some_value') 不会设置会话 cookie(至少在我的设置上不会 - PHP 4.4.1)。假设您定义了一些函数来验证名为 my_func() 的会话 ID,您可能希望在您的 open 函数中执行以下操作

<?php
function _open($save_path, $session_name) {
// 检查会话 ID
$sess_id = session_id();
if (empty(
$sess_id) || !myfunc($sess_id)) {
// 会话 ID 无效 - 生成新 ID
$new_id = md5(uniqid("some random seed here"));
session_id($new_id);
setcookie(session_name(),
$new_id,
0,
"/",
".mydomain.com");
}

return
true;
}
// _open()
?>
harald at hholzer at
15 年前
花了 8 个小时才弄清楚发生了什么……

为了记录在案,因为 php.net 忽略了现实世界

debian 5 默认情况下安装了 php-suhosin 模块,它会改变 session_set_save_handler read/write 函数的行为。

调用会话写入函数时,会话数据将被加密,read 函数返回的字符串将被解密并验证。

加密数据不再与 session_encode/session_decode 兼容。

并且默认情况下会破坏子域处理和多个主机设置,其中使用了不同的文档根目录。

有关更多信息,请查看
http://www.hardened-php.net/suhosin/configuration.html

会话样本数据 (debian 4)
test|s:3:"sdf";

会话样本数据 (debian 5,带有 php-suhosin)
3GdlPEGr2kYgRFDs-pUSoKomZ4fN7r5BM5oKOCMsWNc...

我认为 suhosin 补丁应该在出现无效会话数据时发出警告,以提供有关错误原因的线索。
james dot ellis at gmail dot com
16 年前
在编写自己的会话处理程序,尤其是数据库会话处理程序时,请密切注意垃圾回收以及它如何影响服务器负载。

举个例子,我们选择一个整数

如果您在启用了会话的页面上每分钟有 1000 次请求,每个人都需要启动一个会话,但会话垃圾回收不需要在每次请求时都运行。这样做会导致对数据库服务器进行不必要的查询。

在这个例子中,将您的概率/除数设置为 1/1000 就足以在至少每分钟清理一次旧会话。如果您不需要这种粒度,请增加 gc 除数。

在清除旧会话和服务器负载之间找到平衡点是这里最重要的方面。
information at saunderswebsolutions dot com
17 年前
请注意,如果 php.ini 中的 session.auto_start 设置为 On,您的 session_set_save_handler 将返回 false,因为会话已初始化。

如果您发现您的代码在一台机器上可以正常工作,但在另一台机器上却不能正常工作,请检查 session.auto_start 是否设置为 On
Rusty X
12 年前
重要的是要理解,PHP 的默认基于文件的会话处理会锁定会话文件,本质上只允许一个线程处理任何给定的会话。
当您实现基于数据库的会话存储并且您没有执行任何锁定操作时,您可能会遇到多个线程为同一个会话提供服务的情况,并且您可能会丢失数据,因为第二个线程会覆盖第一个线程完成的任何会话更改。
因此,如果您希望获得与默认基于文件的实现完全相同的行为,您应该考虑以某种方式锁定会话。例如,对于 InnoDB,您可以执行 SELECT ... FOR UPDATE,或者您可以使用 GET_LOCK() 函数。
Balu
19 年前
如果关闭会话,保存处理程序似乎会被重置(在 PHP 4.1.2 中它们会被重置),因此您需要在例如运行 session_write_close() 并使用 session_start() 重新启动会话后再次运行 session_set_save_handler()。
skds1433 at hotmail dot com
15 年前
我犯了一个非常愚蠢的错误。如果您试图调试您的垃圾回收器,请确保在“session_start”之前调用以下代码 >>> BEFORE <<<

<?php
ini_set
('session.gc_probability', 100);
ini_set('session.gc_divisor', 100);
?>

我确信这是 PHP 中的 bug,但事实证明(就像 99% 的时间一样)是我的错。
biba dot vasyl at gmail dot com
6 个月前
如果您使用接口(SessionHandlerInterface)在 mysql 数据库中实现会话存储,那么不清楚 read 方法应该返回什么,因为返回在接口中指定:string|false,也就是说,如果我引用类型
public function read(string $id): string|false {
if ($id) {
$sql = "SELECT `session_id`, `data` FROM `session` WHERE `session_id` = '" . $this->db->escape($id) . "'";
pp($sql);
$query = $this->db->query($sql);
if ($query->num_rows) {
//return (isset($query->one['data']) ? (array)json_decode($query->one['data'], true) : []);
return $query->one['session_id'];
} else {
return '';
}
}
return false;
}
ref.: https://php.net/manual/ru/sessionhandlerinterface.read.php
然后我会收到以下计划的错误响应:错误号:2,消息:session_start(): 无法解码会话对象。会话已销毁
polygon dot co dot in at gmail dot com
1 年前
虽然 session_set_save_handler() 支持通过其他模式保存会话数据,但这不支持在 COOKIE 中保存会话数据的方式。对于具有大量并发请求的网站,session_set_save_handler() 推荐的解决方案不适合网站处理的负载。
所以,另一种解决方法如下。

<?php
// start.php
ob_start(); // 打开输出缓冲

$sessCookieName = session_name();
$_SESSION = json_decode(base64_decode($_COOKIE[$sessCookieName]), true);

// 代码
function echosess() {
echo
$_SESSION['id'];
}
echosess();
$_SESSION['id'] = 1;

// end.php
$op = ob_get_clean(); // 获取当前缓冲区内容并删除当前输出缓冲
$encryptedData = base64_encode(json_encode($_SESSION));
setcookie($sessCookieName, $encryptedData, time() + (ini_get("session.gc_maxlifetime")), '/');
echo
$op;
?>

如果需要更安全的会话数据存储在 cookie 中,则基于密钥的加密/解密可以解决问题。
dimzon541 at gmail dot com
8 年前
将 PHP 会话持久化到 mongodb 中(允许 NLB 无亲和性)
https://gist.github.com/dimzon/62eeb9b8561bcb9f0c6d
polygon dot co dot in at gmail dot com
13 天前
会话数据可以保存到不同的模式中,例如:文件、SQL 和 No SQL 数据库。

但在对会话进行基准测试时,观察到基于文件的会话会导致 inode 枯竭,SQL 数据库会导致性能下降,因为大量的索引会话 ID,而 No SQL 数据库会导致性能问题。

会话基准测试是模拟会话行为,针对可能会使网站崩溃或影响网站性能的攻击。

作为解决方案,可以在通用文件中按如下方式启动会话。

if ( isset( $_COOKIE['PHPSESSID'] ) ) {
// 错误的 cookie 值不会创建新的会话文件/条目。
// 因为它是一个只读操作。
session_start(
[
'read_and_close' => true,
]
);
}

这将启动一个只读会话。

完成此操作后,如果稍后在脚本中需要进行更改(添加/删除)会话数据。可以以以前使用的方式启动会话。

session_start();

并进行必要的修改。

$_SESSION[‘’key] = $value;



unset($_SESSION[‘’some_key]);
polygon dot co dot in at gmail dot com
1 年前
如何使用 cookie 来管理加密的会话数据。

<?php
class MySessionHandler implements SessionHandlerInterface
{
function
__construct()
{
// 将密钥和 IV 存储在安全的地方
//$key = openssl_random_pseudo_bytes(32); // 256 位密钥
//$iv = openssl_random_pseudo_bytes(16); // 128 位 IV

// 将 base64 密钥和 IV 存储在安全的地方
//$key_base64 = base64_encode($key);
//$iv_base64 = base64_encode($vi);

// 在下面使用存储的 base64 密钥和 IV
$key_base64 = 's8Livn/jULM6HDdPY76E3aXtfELdleTaqOC8HgTfW7M=';
$iv_base64 = 'nswqKP23TT+deVNuaV5nXQ==';
$this->key = base64_decode($key_base64);
$this->iv = base64_decode($iv_base64);
}

// 加密
function encryptSess($plaintext)
{
return
openssl_encrypt($plaintext, 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA, $this->iv);
}

// 解密
function decryptSess($ciphertext)
{
return
openssl_decrypt($ciphertext, 'AES-256-CBC', $this->key, OPENSSL_RAW_DATA, $this->iv);
}

public function
open($savePath, $sessionName): bool
{
ob_start(); // 打开输出缓冲
return true;
}

public function
close(): bool
{
return
true;
}

#[
\ReturnTypeWillChange]
public function
read($id)
{
if (isset(
$_COOKIE[session_name()])) {
return (string)
$this->decryptSess(base64_decode($_COOKIE[session_name()]));
} else {
return
'';
}
}

public function
write($id, $data): bool
{
$op = ob_get_clean();
$encryptedData = base64_encode($this->encryptSess($data));
setcookie(session_name(), $encryptedData, time() + (ini_get("session.gc_maxlifetime")), '/');
echo
$op;

return
true;
}

public function
destroy($id): bool
{
return
true;
}

#[
\ReturnTypeWillChange]
public function
gc($maxlifetime)
{
return
true;
}
}

$handler = new MySessionHandler();
session_set_save_handler($handler, true);
session_start();
var_dump($_SESSION);
$_SESSION['id'] = 10000;

echo
'<br/>Hello World';
?>
nickleus
7 年前
我没有看到任何关于例如“open”调用“die”时会发生什么的描述,如“register_shutdown_function” 文档中所述。

“如果您在一个注册的关闭函数中调用 exit(),处理将完全停止,并且不会调用任何其他注册的关闭函数。”

https://php.net/manual/en/function.register-shutdown-function.php

我的结果:相同行为——如果“open”调用“die”/“exit”,则不会调用“read”。
jamesbenson944 at hotmail dot com
11 年前
我没有对保存处理程序使用对象,而是使用函数,但会话写入仍然表现得很奇怪,没有被调用。

但这解决了问题
register_shutdown_function('session_write_close');
anonymous at anonymous dot org
15 年前
如果您只是在每次更改时附加来自会话变量的信息,那么每次变量更改时,就会有很多倍数的变量。一个简单的方法是使用 explode 两次将变量名与其他相关信息分离,然后使用 foreach() 检查存储的集合。这是我编写的用于完成此操作的一段有点混乱的代码。
假设存储的会话变量既在数据库中,也通过函数传递

<?php
$buffer
= array();
$buffer = explode('|',$sessiondata);
$buf1 = array();
$buf2 = array();
$finalbuff = '';
foreach(
$buffer as $i){
$i = explode(';',$i);
foreach(
$i as $b){
array_push($buf1,$b);
}
}
$buffer = explode('|',$result['data']);
foreach(
$buffer as $i){ $i = explode(';',$i); foreach($i as $b){ array_push($buf2,$b);}}
$z = 0;
$x = 0;
while(
$buf2[$z]){
while(
$buf1[$x]){
if(
$buf2[$z] == $buf1[$x]){
$buf2[($z+1)] = $buf1[($x+1)];
}
$x+=2;
}
$z+=2;
}
foreach(
$buf2 as $i){ $finalbuff .= $i; }
?>

$sessiondata 是通过函数传递的变量,$result['data'] 是存储在 SQL 数据库中的数据。
Colin
17 年前
当使用自定义 session 处理程序时,如果第一个回调函数(在我的情况下为 sessOpen)没有找到 session ID,那么在调用第二个参数(在我的情况下为 sessRead)时会设置一个 session ID。
mjohnson at pitsco dot com
18 年前
关于 read 处理程序,文档中说

"Read 函数必须始终返回字符串值,才能使 save
处理程序按预期工作。如果没有任何
数据要读取,则返回空字符串。

我再怎么强调也不为过。我刚花了一整天的时间试图弄清楚为什么我的会话没有存储任何信息。我天真地从 read 处理程序中返回了对数据库查询的结果。由于没有与新 ID 匹配的记录,结果为 NULL。由于它不是字符串,因此会话实际上被禁用了。因此,安全的方法可能是这样的

<?php
function sessRead($id)
{
// 查找数据
$results = getStuff($id);

// 确保它是字符串
settype($results, 'string');
return
$results;
}
?>

当然,您可以对其进行任何操作。但是,无论如何,请确保您返回一个字符串。

希望对您有所帮助!
Michael
coco at digitalco2 dot com
20 年前
当使用 mySQL 处理 session 时,不要忘记调用 mysql_select_db() 以更改数据库,如果您使用单独的数据库存储 session 数据。在每个访问数据库的处理程序函数内部调用 mysql_select_db(),因为如果您在访问另一个数据库后写入 session 数据,它将不会将数据库更改为您的 session 数据库,因此不会写入 session 数据。
spam at skurrilo dot de
22 年前
您不能在 php.ini 中使用

session.save_handler = user

时使用 session 自动启动功能。请改为使用 php.ini 中的 auto_prepend_file 指令,并将其指向您的 save_handler,并在末尾添加 session_start()。
To Top