PHP Conference Japan 2024

session_set_save_handler

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

session_set_save_handler設定使用者層級的 session 儲存函式

描述

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 = ?
): bool

可以註冊以下原型

session_set_save_handler(object $sessionhandler, bool $register_shutdown = true): bool

session_set_save_handler() 設定使用者層級的 session 儲存函式,這些函式用於儲存和擷取與 session 關聯的資料。當偏好使用 PHP sessions 提供的儲存方法以外的其他方法時,這非常有用,例如,將 session 資料儲存在本機資料庫中。

參數

此函式有兩個原型。

sessionhandler

實作 SessionHandlerInterface,以及可選的 SessionIdInterface 和/或 SessionUpdateTimestampHandlerInterface 的類別的實例,例如 SessionHandler,以註冊為 session 處理器。

register_shutdown

session_write_close() 註冊為 register_shutdown_function() 函式。

open

具有以下簽名的可呼叫物件

open(string $savePath, string $sessionName): bool

open 回呼函數的作用類似於類別中的建構函式,在 session 開啟時執行。當 session 自動或手動使用 session_start() 啟動時,它是第一個執行的回呼函式。成功時傳回值為 true,失敗時為 false

close

具有以下簽名的可呼叫物件

close(): bool

close 回呼函數的作用類似於類別中的解構函式,在 session 寫入回呼函數被呼叫後執行。當呼叫 session_write_close() 時,也會呼叫它。成功時傳回值應為 true,失敗時應為 false

read

具有以下簽名的可呼叫物件

read(string $sessionId): string

read 回呼函數必須始終傳回 session 編碼 (序列化) 的字串,如果沒有資料要讀取,則傳回空字串。

當 session 啟動或呼叫 session_start() 時,PHP 會在內部呼叫此回呼函數。在呼叫此回呼函數之前,PHP 將呼叫 open 回呼函數。

此回呼函數傳回的值必須與最初傳遞給 write 回呼函數以進行儲存的序列化格式完全相同。傳回的值將由 PHP 自動反序列化,並用於填入 $_SESSION 超級全域變數。雖然資料看起來與 serialize() 類似,但請注意,它是 session.serialize_handler ini 設定中指定的不同格式。

write

具有以下簽名的可呼叫物件

write(string $sessionId, string $data): bool

當需要儲存和關閉 session 時,會呼叫 write 回呼函數。此回呼函數會接收目前的 session ID,以及 $_SESSION 超級全域變數的序列化版本。PHP 內部使用的序列化方法在 session.serialize_handler ini 設定中指定。

傳遞給此回呼函數的序列化 session 資料應根據傳遞的 session ID 儲存。擷取此資料時,read 回呼函數必須傳回最初傳遞給 write 回呼函數的完全相同的值。

當 PHP 關閉或明確呼叫 session_write_close() 時,會呼叫此回呼函數。請注意,在執行此函式後,PHP 將在內部執行 close 回呼函數。

注意:

「write」處理器在輸出串流關閉後才會執行。因此,永遠不會在瀏覽器中看到「write」處理器中偵錯陳述式的輸出。如果需要偵錯輸出,建議將偵錯輸出寫入檔案。

destroy

具有以下簽名的可呼叫物件

destroy(string $sessionId): bool

當使用 session_destroy() 或使用 destroy 參數設定為 truesession_regenerate_id() 銷毀 session 時,會執行此回呼函數。成功時傳回值應為 true,失敗時應為 false

gc

具有以下簽名的可呼叫物件

gc(int $lifetime): bool

為了清除舊的 session 資料,PHP 會定期在內部呼叫垃圾回收器回呼函數。頻率由 session.gc_probabilitysession.gc_divisor 控制。傳遞給此回呼函數的 lifetime 值可以在 session.gc_maxlifetime 中設定。成功時傳回值應為 true,失敗時應為 false

create_sid

具有以下簽名的可呼叫物件

create_sid(): string

當需要新的 session ID 時,會執行此回呼函數。不提供任何參數,傳回值應為適用於您的處理器的有效 session ID 的字串。

validate_sid

具有以下簽名的可呼叫物件

validate_sid(string $key): bool

當啟用 session.use_strict_mode 時,且提供了一個 session ID,並要啟動 session 時,會執行此回呼函式。key 參數是要驗證的 session ID。如果具有該 ID 的 session 已存在,則該 session ID 有效。成功時應傳回 true,失敗時應傳回 false

update_timestamp

具有以下簽名的可呼叫物件

update_timestamp(string $key, string $val): bool

當 session 更新時,會執行此回呼函式。key 參數是 session ID,val 參數是 session 資料。成功時應傳回 true,失敗時應傳回 false

傳回值

成功時傳回 true,失敗時傳回 false

範例

範例 #1 自訂 session 處理器:請參閱 SessionHandlerInterface 概要中的完整程式碼。

我們僅在此展示調用,完整的範例可以在上面連結的 SessionHandlerInterface 概要中看到。

請注意,我們使用物件導向程式設計原型與 session_set_save_handler(),並使用函數的參數旗標註冊關機函數。當註冊物件作為 session 儲存處理器時,通常建議這樣做。

<?php
class MySessionHandler implements SessionHandlerInterface
{
// 在此實作介面
}

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

// 繼續使用 $_SESSION 中的鍵值設定和擷取值

注意事項

警告

writeclose 處理器會在物件解構後被呼叫,因此無法使用物件或拋出例外。由於例外狀況不會被捕捉,也不會顯示任何例外狀況追蹤,執行將會意外終止,所以無法捕捉例外。但是,物件解構器可以使用 session。

可以從解構器呼叫 session_write_close() 來解決這個雞生蛋還是蛋生雞的問題,但最可靠的方法是如上所述註冊關機函數。

警告

如果在指令碼終止時關閉 session,某些 SAPI 會變更目前的工作目錄。可以使用 session_write_close() 提前關閉 session。

參見

新增註解

使用者貢獻的註解 41 則註解

andreipa at gmail dot com
9 年前
在花費了這麼多時間來理解 PHP session 如何與資料庫一起運作,並嘗試了許多次都無法正確運作後,我決定重寫我們的朋友 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
//page 1
require_once('inc.session.php');

session_start();

$_SESSION['var1'] = "My Portuguese text: 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。它的傳回值對於有效的 session id $sessionId 應為 true,對於無效的 session id $sessionId 應為 false。如果傳回 false,則會產生一個新的 session id 以取代無效的 session id $sessionId。

update_timestamp($sessionId)
此回呼是用來更新時間戳記,其傳回值應在成功時為 true,失敗時為 false。

如果您使用此原型,如果您提供的參數少於 6 個,或者您提供的參數多於 session_set_save_handler() 所接受的參數,您將會收到「session_set_save_handler() 的參數計數錯誤」的警告。

如果您使用 session_set_save_handler(SessionHandlerInterface $sessionhandler [, bool $register_shutdown = true ] ) 的 OOP 原型,即使在 PHP 7.2 中,也不會調用 $sessionhandler 類別中名為 validate_sid 或 update_timestamp 的成員方法,但自 PHP 5.5.1 起支援名為 create_sid 的成員方法。

今天是 2017 年 12 月 16 日,文件甚至 PHP 可能會在之後的某個時間更新。
peter at brandrock dot co dot za
6 年前
如果像此頁面上的範例一樣儲存到資料庫,請考慮以下事項以提高效能。

使用 SessionExpires 欄位上的索引來建立 Sessions 表格,以便在垃圾收集階段快速識別要刪除的資料列。

最好在每次 session 開始/開啟時執行垃圾收集「delete from sessions where expiresOn < $now」。如果您在過期時間上有索引,這不會有太大的影響,並且可以讓所有使用者平均分攤負載。如果有可能大量 session 同時過期,請加入「limit 100」子句,設定為任何合理的數字,以便每個使用者分攤負載。

使用 varchar 而非 Text 來儲存資料,因為 Text 會將欄位儲存在頁面之外,且檢索速度稍慢。只有當您的應用程式確實將大量文字儲存在 session 中時,才使用 Text。
ohcc at 163 dot com
6 年前
自 PHP 7.0 起,您可以實作 SessionUpdateTimestampHandlerInterface 來
在 session_set_save_handler() 的非 OOP 原型中,定義您自己的 session id 驗證方法,例如 validate_sid,以及時間戳記更新方法,例如 update_timestamp。

SessionUpdateTimestampHandlerInterface 是 PHP 7.0 中引入的新介面,尚未被記錄下來。它有兩個抽象方法:SessionUpdateTimestampHandlerInterface :: validateId($sessionId) 和 SessionUpdateTimestampHandlerInterface :: updateTimestamp($sessionId, $sessionData)。

<?php
/*
@author Wu Xiancheng
僅適用於 PHP 7.0+ 的程式碼結構,因為 SessionUpdateTimestampHandlerInterface 是在 PHP 7.0 中引入的
使用此類別,您可以使用 PHP 7.0+ 中 session_set_save_handler() 的 OOP 原型來驗證 php session id 並更新 php session 資料的時間戳記
*/
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){
// 傳回值應為 session 資料或空字串
// ...
}
public function
write($sessionId, $sessionData){
// 傳回值在成功時應為 true,失敗時應為 false
// ...
}
public function
create_sid(){
// 自 PHP 5.5.1 起可用
// 當需要新的 session id 時會在內部調用
// 不需要參數,且傳回值應為建立的新 session id
// ...
}
public function
validateId($sessionId){
// 實作 SessionUpdateTimestampHandlerInterface::validateId()
// 自 PHP 7.0 起可用
// 如果 session id 有效,則傳回值應為 true,否則為 false
// 如果傳回 false,php 將在內部產生新的 session id
// ...
}
public function
updateTimestamp($sessionId, $sessionData){
// 實作 SessionUpdateTimestampHandlerInterface::validateId()
// 自 PHP 7.0 起可用
// 傳回值在成功時應為 true,失敗時應為 false
// ...
}
}
?>
polygon dot co dot in at gmail dot com
3 年前
以下是一個示範,用來檢查 session 函式的執行順序。

<?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
Steven George
10 年前
請注意,除了在呼叫 write() 和 close() 之前解構物件之外,PHP 似乎也會解構類別。也就是說,您甚至無法在 write() 和 close() 處理程式中呼叫外部類別的靜態方法 - PHP 會發出「找不到類別 xxxx」的嚴重錯誤。
tomas at slax dot org
16 年前
關於 SAPI:函式描述中提到的警告(某些 SAPI 會變更目前的工作目錄)非常重要。

這表示,如果您的回呼「write」函式需要寫入目前目錄中的檔案,它將找不到該檔案。您必須使用絕對路徑,而不能依賴目前的工作目錄。

我以為這個警告僅適用於某些奇怪的環境(例如 Windows),但它在 Linux + Apache 2.2 + PHP 5 上也會發生。
tony at marston-home dot demon dot co dot uk
6 年前
您的自訂 session 處理程式不應包含對任何 session 函式的呼叫,例如 session_name() 或 session_id(),因為相關的值會作為各種處理程式方法的引數傳遞。嘗試從其他來源取得值可能無法如預期運作。
centurianii at yahoo dot co dot uk
7 年前
新增自:andreipa at gmail dot com 的非常有用的類別

1. 您應從 SessionHandlerInterface 方法處理 session 過期和資料 I/O,
2. 您不應從這些方法處理 session 重新產生和資料修改,而是從靜態方法處理,例如 sth like 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();

我花了好幾個小時追蹤我的錯誤,發現第三次 Session::read() 結束時使用了 null 的 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
22 年前
僅用幾句話來解釋使用 session_set_save_handler() 時遇到的一些問題。似乎 PHP 內部會按照以下順序呼叫會話管理函式:open()、read()、write()、close()。即使您沒有呼叫 sesison_start(),也會呼叫 Close() 函式,可能是為了某些原因,例如清理。
如果您嘗試重新定義這些函式並呼叫 `sessions_set_save_handler()`,但出現問題(以我的情況來說,session 資料沒有被寫入),最好依照它們被呼叫的順序來偵錯。它們不會在瀏覽器上產生錯誤輸出,但您可以使用 `print` 或 `echo` 來除錯。
簡而言之,如果您的 `write()` 函式沒有如預期運作,請檢查先前的函式 `open()` 和 `read()` 是否有錯誤。
我希望這能幫助某些人節省幾個小時的偵錯時間。
oliver at teqneers dot de
19 年前
對於某些人來說,可能需要知道,如果標準 session 處理器已被 `session_set_save_handler` 覆寫,則鎖定機制將不再運作(在 `session_read` 和 `session_write` 之間)。可能會發生以下情況:

腳本 "A" 開始。
讀取 session 資料。
。腳本 "B" 開始
。讀取 session 資料
執行(30 秒)並新增 session 資料
。寫入 session 資料
。腳本 "B" 停止
寫入 session 資料。
腳本 "A" 停止。

如果腳本 "A" 執行很長時間(例如 30 秒),同一個使用者可能會啟動另一個也使用 session 的腳本 "B"。腳本 "B" 將會啟動並讀取 session 資料,即使腳本 "A" 仍在執行。由於腳本 "B" 速度快很多,它將會完成其工作,並在腳本 "A" 結束之前寫回其 session 資料。現在腳本 "A" 結束,並覆寫了腳本 "B" 的所有 session 資料。如果您不使用 `session_set_save_handler`,則不會發生這種情況,因為在這種情況下,PHP 將不會啟動腳本 "B",直到腳本 "A" 結束。
e dot sand at elisand dot com
15 年前
session 資料中的「二進位」資料似乎圍繞著類別/物件名稱,如果您將 session 資料傳遞給某個函式以清理 SQL 注入,您確實可能會遇到問題。

例如,使用 `PDO::quote()` 函式來準備用於注入的資料(在我的情況下是針對 SQLite3),當它遇到第一段二進位資料時就會停止,導致我的 session 資訊損壞。

此變更*一定*發生在 5.2 系列的某處,因為我最近才在一個已在較早版本的 PHP 5.2 上測試過並能正常運作的程式碼庫中遇到這個問題。

這實際上可能是一個錯誤 - 我尚未檢查...但請注意,或許使用 base64 來編碼/解碼您的 session 資料是個好主意,以確保安全(儘管您現在無法在儲存層級上視覺檢查序列化的 session 資訊,這對即時偵錯 session 來說是個相當大的問題)。
james at dunmore dot me dot uk
17 年前
我認為在這裡強調 `WRITE` 方法應該使用 `UPDATE+INSERT`(或 MySQL 特有的 `REPLACE`)非常重要。

「外面」有一些範例程式碼僅使用 `UPDATE` 來作為寫入方法,在這種情況下,當呼叫 `session_regenerate_id` 時,session 資料會遺失(因為更新會失敗,因為索引鍵已變更)。

我剛因為這個問題浪費了一整天的時間(我知道我應該仔細思考/RTFM,但這是一個很容易陷入的陷阱)。
frank at interactinet dot com
13 年前
我提交 session 資料時遇到了問題。
要在不關閉 session 的情況下「提交並繼續」,請將此程式碼放在 `write` 方法的頂部:

<?php

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

?>

請注意,任何時候 php 產生新的 session 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
18 年前
請注意,基於安全考量,Debian 和 Ubuntu 發行版的 php 不會呼叫 `_gc` 來移除舊的 session,而是執行 `/etc/cron.d/php*`,它會檢查 `php.ini` 中 `session.gc_maxlifetime` 的值,並刪除 `/var/lib/php*` 中的 session 檔案。這一切都沒有問題,但這表示如果您撰寫自己的 session 處理器,您將需要明確地呼叫自己的 `_gc` 函式。一個適合執行此操作的地方是您的 `_close` 函式,如下所示:

<?php
function _close() {
_gc(get_cfg_var("session.gc_maxlifetime"));
// 函式的其餘部分放在這裡
}
?>
dummynick at gmail dot com
14 年前
我遇到了致命錯誤:拋出了沒有堆疊框架的異常,而且我花了幾天的時間才找出原因。我使用 memcache 來儲存 session,而在我的自訂類別中,我在寫入方法中使用 Memcache 類別。

我將寫入方法中的程式碼放在 try-catch 區塊中,這解決了我的問題。
joel the usual at sign then purerave.com
15 年前
將 session 儲存在資料庫中時,通常使用現有的自訂資料庫物件會有所幫助,但這會在最新版本的 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` 之前使用。如果您的 session 設定為自動啟動,它將回傳 `FALSE` 值。因此,您需要在 `session_set_save_handler` 之前新增 `session_write_close()` 以取消 session 的自動啟動。它看起來像這樣:

<?php
/*
qq:290359552
*/
session_write_close(); //取消 session 的自動啟動,很重要

function open()
{
...
}
....
session_set_save_handler( ... );
session_start();
?>
bart2yk at yahoo dot com
14 年前
您可以在資料庫物件的解構函數中呼叫 `session_write`,以確保您仍然有與 mysql 的連線,並且 session 已寫入。
sneakyimp AT hotmail DOT com
18 年前
這些函式的行為、回傳值以及確切的呼叫時間在這裡的文件記錄相當差。我想人們可能會想知道:

1) 呼叫 `session_start()` 會觸發 PHP 首先呼叫您的 `open` 函式,然後在繼續執行緊接在 `session_start()` 呼叫之後的程式碼之前,呼叫您的 `read` 函式。

2) 在您的 `open` 函式內呼叫 `session_id('some_value')` 將*不會*設定 session Cookie(至少在我的設定中 - PHP 4.4.1)。假設您定義了一個名為 `my_func()` 的函式來驗證 session id,您可能需要在您的 `open` 函式中執行類似以下的操作:

<?php
function _open($save_path, $session_name) {
// 檢查 session id
$sess_id = session_id();
if (empty(
$sess_id) || !myfunc($sess_id)) {
//session_id 無效 - 產生新的
$new_id = md5(uniqid("在這裡放一些隨機種子"));
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` 的讀取/寫入函式的行為。

在呼叫 session 寫入函式時,session 資料將會被加密,並且 `read` 函式回傳的字串會被解密和驗證。

加密後的資料不再與 `session_encode/session_decode` 相容。

並會預設中斷子網域處理和使用不同文件根目錄的多主機設定。

如需更多資訊,請參考:
http://www.hardened-php.net/suhosin/configuration.html

session 範例資料 (debian 4)
test|s:3:"sdf";

session 範例資料 (debian 5, 使用 php-suhosin)
3GdlPEGr2kYgRFDs-pUSoKomZ4fN7r5BM5oKOCMsWNc...

我認為 suhosin 修補程式在出現無效 session 資料時應該報告警告,以提示發生什麼問題。
james dot ellis at gmail dot com
16 年前
在撰寫您自己的 session 處理器時,特別是資料庫 session 處理器,請密切注意垃圾清理以及它如何影響伺服器負載。

舉一個概數範例:

如果你的啟用 Session 的頁面每分鐘有 1000 個請求,那麼每個請求都需要啟動一個 Session,但是 Session 的垃圾回收並不需要在每個請求都執行。這樣做會在資料庫伺服器上造成不必要的查詢。

在這個例子中,將你的機率/除數設定為 1/1000 就足以至少每分鐘清理一次舊的 Session。 如果你不需要這麼高的精細度,請增加 gc 除數。

在這裡,找到清理舊 Session 和伺服器負載之間的平衡點是很重要的。
information at saunderswebsolutions dot com
18 年前
請注意,如果在 php.ini 中將 session.auto_start 設定為 On,你的 session_set_save_handler 將會回傳 false,因為 Session 已經被初始化了。

如果你發現你的程式碼在一台機器上運作正常,但在另一台機器上卻無法運作,請檢查 session.auto_start 是否設定為 On。
Rusty X
12 年前
重要的是要理解 PHP 預設的基於檔案的 Session 處理會鎖定 Session 檔案,因此本質上一次只允許一個執行緒處理給定的 Session。
當你實作一個以資料庫為後端的 Session 儲存,並且沒有進行任何鎖定時,你可能會遇到多個執行緒同時處理同一個 Session 的情況,而且你可能會遺失資料,因為第二個執行緒將覆寫第一個執行緒所做的任何 Session 變更。
因此,如果你希望擁有與預設基於檔案的實作完全相同的行為,你應該考慮以某種方式鎖定 Session。例如,使用 InnoDB,你可以執行 SELECT ... FOR UPDATE,或者你可以使用 GET_LOCK() 函數。
Balu
20 年前
如果 Session 關閉,save-handlers 似乎會被重置 (在 PHP 4.1.2 中是這樣),所以你需要在執行 session_write_close() 並使用 session_start() 重新啟動 Session 後再次執行 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 的錯誤,但結果證明(像 99% 的時間一樣)是我自己的錯誤。
biba dot vasyl at gmail dot com
9 個月前
如果你使用介面 (SessionHandlerInterface) 來實作將 Session 儲存在 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.dev.org.tw/manual/ru/sessionhandlerinterface.read.php
那麼我會收到以下計劃的錯誤回應:錯誤 #: 2,訊息:session_start():無法解碼 Session 物件。Session 已被銷毀
polygon dot co dot in at gmail dot com
1 年前
儘管 session_set_save_handler() 支援透過其他模式儲存 Session 資料,但這並不支援以 COOKIES 儲存 Session 資料的方式。對於具有大量並行請求的網站,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 中 Session 資料安全性,則可以使用基於金鑰的加密/解密來解決此問題。
dimzon541 at gmail dot com
9 年前
將 PHP Session 持久化到 mongodb (允許 NLB 而無需親和性)
https://gist.github.com/dimzon/62eeb9b8561bcb9f0c6d
polygon dot co dot in at gmail dot com
4 個月前
Session 資料可以以不同的模式儲存,例如:檔案、SQL 和 NoSQL 資料庫。

但是在基準測試 Session 時,發現基於檔案的 Session 會導致 inode 耗盡,SQL 資料庫由於大量索引的 Session ID 而導致效能較慢,而 NoSQL 資料庫則會導致效能問題。

基準測試 Session 是模擬攻擊中 Session 的行為,這些攻擊可能會使網站癱瘓或影響網站效能。

為了解決這個問題,可以在通用檔案中如下啟動 Session。

if ( isset( $_COOKIE['PHPSESSID'] ) ) {
// 錯誤的 Cookie 值不會建立新的 Session 檔案/條目。
// 因為它是唯讀的。
session_start(
[
'read_and_close' => true,
]
);
}

這將啟動一個唯讀 Session。

完成此操作後,稍後在腳本中需要進行更改 (新增/移除) Session 資料時,可以像之前一樣以正常方式啟動 Session。

session_start();

並進行所需的修改。

$_SESSION[‘’key] = $value;

或者

unset($_SESSION[‘’some_key]);
polygon dot co dot in at gmail dot com
1 年前
如何使用 Cookie 來管理加密的 Session 資料。

<?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.dev.org.tw/manual/en/function.register-shutdown-function.php

我的結果:行為相同——如果 "open" 呼叫 "die"/"exit",則不會呼叫 "read"。
jamesbenson944 at hotmail dot com
11 年前
我沒有使用物件來處理儲存處理器,而是使用函式,但仍然遇到 session 寫入未被呼叫的奇怪行為。

不過,這解決了問題
register_shutdown_function('session_write_close');
anonymous at anonymous dot org
16 年前
如果你每次都只是附加 session 變數中的資訊,那麼每次變數被更改時,你都會有很多重複的變數。一個簡單的方法是將資料分解兩次,將變數名稱與其他相關資訊分開,然後 foreach() 檢查已儲存的集合。這是我寫的一小段混亂程式碼來實現這一點。
假設 session 變數儲存在資料庫中,並透過函式傳遞

<?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)時會設定一個 id。
mjohnson at pitsco dot com
18 年前
關於讀取處理器,文件說

"讀取函式必須始終返回字串值,才能使儲存
處理器按預期工作。如果沒有
資料要讀取,則返回空字串。"

我再怎麼強調都不為過。我花了半天的時間才搞清楚為什麼我的 Session 無法儲存任何資訊。我當時很輕率地從讀取處理常式 (read handler) 返回資料庫查詢的結果。由於新的 ID 沒有匹配的項目,所以結果是 NULL。因為結果不是字串,Session 基本上就被停用了。所以,比較安全的方式可能是這樣:

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

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

當然,你可以隨意使用它。但是,無論如何,請確保你返回的是一個字串。

希望能幫到你,
Michael
coco at digitalco2 dot com
21 年前
當使用 mySQL 作為你的 Session 處理函數時,別忘了呼叫 mysql_select_db() 來切換資料庫,如果你是使用獨立的資料庫來儲存 Session 資料。請在每一個會存取資料庫的處理常式函式內呼叫 mysql_select_db(),因為如果你在存取另一個資料庫之後才寫入 Session 資料,它不會將資料庫切換到你的 Session 資料庫,因此就不會寫入 Session 資料。
spam at skurrilo dot de
22 年前
你不能使用 Session 自動啟動功能搭配

session.save_handler = user

在你的 php.ini 中設定。請改用 php.ini 中的 auto_prepend_file 指令,並將其指向你的 save_handler,最後加上 session_start()。
To Top