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 形式)。

當一個普通的 SessionHandler 實例使用 session_set_save_handler() 設定為儲存處理器時,它會包裝目前的儲存處理器。繼承自 SessionHandler 的類別允許您覆寫這些方法,或者透過呼叫最終包裝內部 PHP session 處理器的父類別方法來攔截或過濾它們。

例如,這允許您攔截 readwrite 方法來加密/解密 session 資料,然後將結果傳遞給父類別並從父類別接收結果。或者,您可以選擇完全覆寫一個方法,例如垃圾回收回呼 gc

由於 SessionHandler 包裝了目前的內部儲存處理器方法,上述加密的範例可以應用於任何內部儲存處理器,而無需了解處理器的內部結構。

要使用這個類別,首先使用 session.save_handler 設定您想要公開的儲存處理器,然後將 SessionHandler 的實例或其延伸的實例傳遞給 session_set_save_handler()

請注意,此類別的回呼方法設計為由 PHP 內部呼叫,並不打算從使用者空間程式碼呼叫。返回值同樣由 PHP 內部處理。有關 session 工作流程的更多資訊,請參考 session_set_save_handler()

類別概要

類別 SessionHandler 實作 SessionHandlerInterfaceSessionIdInterface {
/* 方法 */
公開 close(): 布林值
公開 create_sid(): 字串
公開 destroy(字串 $id): 布林值
公開 gc(整數 $max_lifetime): 整數|false
公開 open(字串 $path, 字串 $name): 布林值
公開 read(字串 $id): 字串|false
公開 write(字串 $id, 字串 $data): 布林值
}

注意事項

警告

這個類別的設計目的是為了公開目前的內部 PHP 工作階段儲存處理器。如果您想要撰寫自己的自訂儲存處理器,請實作 SessionHandlerInterface 介面,而不是繼承自 SessionHandler

範例

範例 #1 使用 SessionHandler 將加密功能添加到內部 PHP 儲存處理器。

<?php

/**
* decrypt AES 256
*
* @param data $edata
* @param string $password
* @return decrypted data
*/
function decrypt($edata, $password) {
$data = base64_decode($edata);
$salt = substr($data, 0, 16);
$ct = substr($data, 16);

$rounds = 3; // depends on key length
$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);
}

/**
* crypt AES 256
*
* @param data $data
* @param string $password
* @return base64 encrypted data
*/
function encrypt($data, $password) {
// Generate a cryptographically secure random salt using random_bytes()
$salt = random_bytes(16);

$salted = '';
$dx = '';
// Salt the key(32) and iv(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);
}
}

// we'll intercept the native 'files' handler, but will equally work
// with other internal native handlers like 'sqlite', 'memcache' or 'memcached'
// which are provided by PHP extensions.
ini_set('session.save_handler', 'files');

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

// proceed to set and retrieve values by key from $_SESSION

注意事項:

由於這個類別的方法設計為由 PHP 在正常工作階段流程中內部呼叫,因此子類別對父類別方法的呼叫(也就是實際的內部原生處理器)將會回傳 false,除非工作階段已經實際啟動(自動啟動或透過明確呼叫 session_start() 啟動)。在撰寫單元測試,且類別方法可能會被手動呼叫的情況下,這一點非常重要。

目錄

新增註記

使用者貢獻的註記 8 則註記

rasmus at mindplay dot dk
9 年前
由於工作階段處理器的生命週期相當複雜,我發現僅用文字說明難以理解 - 因此我追蹤了對自訂 SessionHandler 進行的函式呼叫,並建立了這個關於呼叫各種工作階段方法時究竟會發生什麼的概述

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 年前
設定您自己的 session handler 的最佳方法是繼承原生 SessionHandler(根據您的需要覆寫 7 個方法和建構子,保持相同的簽章)。或者,如果您打算使用「lazy_write」,您也可以實作 SessionUpdateTimestampHandlerInterface。

選項 1

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

選項 2

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

我不建議這樣做

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

如果您好奇,以下是呼叫方法的順序(在 Windows 11 64 位元上使用 PHP 8.2 和 XAMPP v3.3.0)

- open(總是會被呼叫)

- validateId 和/或 create_sid

如果您實作 SessionUpdateTimestampHandlerInterface,則會呼叫 validateId。如果驗證失敗,則會呼叫 create_sid。

如果需要新的 session ID,則會呼叫 create_sid:新的 session 等。

- 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 session handler 生命週期(更新至 7.0 或更高版本)的完整概述。尤其是我想強調在使用原生 PHP 函數進行 session 管理時,會呼叫哪些方法以及以何種順序呼叫。

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

我希望這個分析能幫助所有有興趣深入了解 PHP 執行原生 session 管理以及自定義 session handler 應該做什麼的開發人員。
歡迎任何評論或建議。
RomanV
7 個月前
請注意,當您繼承 \SessionHandler 時,您會隱式停用 session 嚴格模式,因為它沒有實作 SessionUpdateTimestampHandlerInterface(請參閱:https://php.dev.org.tw/manual/en/session.configuration.php#ini.session.use-strict-mode),因此不會呼叫 validateId()。

每當任何權限級別發生更改時(例如,在身份驗證、密碼/權限/使用者角色更改期間),呼叫 session_regenerate_id() 函數以減輕 session 固定攻擊至關重要!
jeremie dot legrand at komori-chambon dot fr
8 年前
這是一個包裝器,用於將每個 session 的操作記錄到檔案中。這對於調查 session 鎖定(阻止 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]; 將請求 URI 以 '?' 字元分割成最多兩個部分,並取第一個部分(也就是不包含查詢字串的 URI)賦值給 $base_uri 變數。*/
/* $hdl = fopen($this->fich, 'a'); 以附加模式 ('a') 開啟檔案 $this->fich,並將檔案控制代碼賦值給 $hdl 變數。*/
/* fwrite($hdl, date('Y-m-d h:i:s').' '.$base_uri.' : '.$action."\n"); 將日期時間、$base_uri 和 $action 的值寫入由 $hdl 指定的檔案,並加上換行符號。*/
/* fclose($hdl); 關閉由 $hdl 指定的檔案。*/
}
}
/* ini_set('session.save_handler', 'files'); 設定 session 的儲存處理器為檔案系統。*/
/* $handler = new DumpSessionHandler('/path/to/dump_sessions.log'); 建立一個 DumpSessionHandler 物件,並將記錄檔路徑指定為 '/path/to/dump_sessions.log'。*/
/* session_set_save_handler($handler, true); 設定 session 的儲存處理器為先前建立的 $handler 物件。*/
/* 建立一個連結到 #123892 的錨點,顯示電子郵件地址 wei.kavin@gmail.com,並加上段落符號。*/
/* 5 年前 */
/* php -S localhost:8000 -t foo/ 使用 PHP 內建伺服器,監聽本地主機的 8000 埠,並將 foo/ 目錄設定為文件根目錄。*/

/* touch index.php 建立或更新 index.php 檔案的時間戳記。*/

/* vi index.php 使用 vi 編輯器開啟 index.php 檔案。*/
============================================================
/* class NativeSessionHandler extends \SessionHandler 定義一個繼承自 \SessionHandler 的名為 NativeSessionHandler 的類別。*/
{
/* public function __construct($savePath = null) 建構函數,可選擇性地傳入儲存路徑 $savePath。*/
{
/* if (null === $savePath) { 如果未提供 $savePath,則… */
/* $savePath = ini_get('session.save_path'); 從 php.ini 檔案取得 session.save_path 的值。*/
}

/* $baseDir = $savePath; 將 $savePath 的值賦值給 $baseDir。*/

/* if ($count = substr_count($savePath, ';')) { 如果 $savePath 中包含 ';' 字元,則… */
/* if ($count > 2) { 如果 ';' 字元出現次數超過 2 次,則… */
/* throw new \InvalidArgumentException(sprintf('Invalid argument $savePath \'%s\'', $savePath)); 拋出 InvalidArgumentException 例外,表示 $savePath 參數無效。*/
}

/* // 最後一個 ';' 字元後面的字元是路徑 */
/* $baseDir = ltrim(strrchr($savePath, ';'), ';'); 取得 $savePath 中最後一個 ';' 字元後面的部分,並去除左邊的 ';' 字元,賦值給 $baseDir。*/
}

/* if ($baseDir && !is_dir($baseDir) && !@mkdir($baseDir, 0777, true) && !is_dir($baseDir)) { 如果 $baseDir 存在,但不是目錄,且建立目錄失敗,則… */
/* throw new \RuntimeException(sprintf('Session Storage was not able to create directory "%s"', $baseDir)); 拋出 RuntimeException 例外,表示無法建立 session 儲存目錄。*/
}

/* ini_set('session.save_path', $savePath); 設定 session.save_path 的值。*/
/* ini_set('session.save_handler', 'files'); 設定 session 的儲存處理器為檔案系統。*/
}
}

/* $handler = new NativeSessionHandler("/var/www/foo"); 建立一個 NativeSessionHandler 物件,並指定儲存路徑為 /var/www/foo。*/
/* session_set_save_handler($handler, true); 設定 session 的儲存處理器為先前建立的 $handler 物件。*/
/* session_start(); 啟動 session。*/
/* $a = $handler->write("aaa","bbbb");var_dump($a);exit; 寫入 session 資料,並印出結果,然後結束程式。*/

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

/* 輸出:bool(false) */
To Top