2010-06-08 72 views
13

默認情況下,即使會話中沒有數據,PHP的會話處理機制也會設置會話Cookie標頭並存儲會話。如果在會話中沒有設置數據,那麼我不希望在響應中發送到客戶端的Set-Cookie頭文件,並且我不希望在服務器上存儲空的會話記錄。如果數據被添加到$_SESSION,那麼正常行爲應該繼續。我應該如何在PHP中實現惰性會話創建?

我的目標是實現的Drupal 7和Pressflow其中沒有會話被存儲(或會話cookie頭中發送)之類的懶惰會話創建行爲除非數據被添加到應用程序執行期間的$_SESSION陣列。此行爲的目的是允許反向代理(例如Varnish)緩存和提供匿名流量,同時讓已認證的請求通過Apache/PHP。 Varnish(或其他代理服務器)被配置爲在沒有cookie的情況下通過任何請求,假設如果存在cookie,則該請求適用於特定客戶端。

我已經從使用session_set_save_handler()和覆蓋的session_write()執行保存前請檢查$_SESSION陣列中的數據,並會在這裏把這件事寫成庫,並添加一個答案,如果這是最好的Pressflow移植的會話處理代碼/只有路線採取。

我的問題:雖然我可以實現完全定製session_set_save_handler()系統,有更簡單的方式在一個相對通用的辦法,這將是透明於大多數應用程序得到這個懶惰的會話創建的行爲嗎?

+0

@cletus:他說他的理由:*此行爲的關鍵是允許像Varnish這樣的反向代理緩存並提供匿名流量,同時讓認證請求通過Apache/PHP。* – webbiedave 2010-06-08 14:33:15

+0

相關答案:[PHP Session類與CodeIgniter會話類類似?](http://stackoverflow.com/questions/ 11596082/php-session-class-similar-codeigniter-session-class/11596538#11596538) – hakre 2012-08-01 08:21:30

回答

3

我開發了一個working solution這個問題,它使用session_set_save_handler()和一組自定義會話存儲方法,在寫出會話數據之前檢查$_SESSION數組中的內容。如果沒有要寫入會話的數據,則使用header('Set-Cookie:', true);來防止PHP的會話cookie被髮送回應

此代碼的最新版本以及文檔和示例爲available on GitHub。在下面的代碼中,使這項工作的重要功能是lazysess_read($id)lazysess_write($id, $sess_data)

<?php 
/** 
* This file registers session save handlers so that sessions are not created if no data 
* has been added to the $_SESSION array. 
* 
* This code is based on the session handling code in Pressflow (a backport of 
* Drupal 7 performance features to Drupal 6) as well as the example code described 
* the PHP.net documentation for session_set_save_handler(). The actual session data 
* storage in the file-system is directly from the PHP.net example while the switching 
* based on session data presence is merged in from Pressflow's includes/session.inc 
* 
* Links: 
*  http://www.php.net/manual/en/function.session-set-save-handler.php 
*  http://bazaar.launchpad.net/~pressflow/pressflow/6/annotate/head:/includes/session.inc 
* 
* Caveats: 
*  - Requires output buffering before session_write_close(). If content is 
*  sent before shutdown or session_write_close() is called manually, then 
*  the check for an empty session won't happen and Set-Cookie headers will 
*  get sent. 
*   
*  Work-around: Call session_write_close() before using flush(); 
*   
*  - The current implementation blows away all Set-Cookie headers if the 
*  session is empty. This basic implementation will prevent any additional 
*  cookie use and should be improved if using non-session cookies. 
* 
* @copyright Copyright &copy; 2010, Middlebury College 
* @license http://www.gnu.org/copyleft/gpl.html GNU General Public License (GPL), Version 3 or later. 
*/ 

/********************************************************* 
* Storage Callbacks 
*********************************************************/ 

function lazysess_open($save_path, $session_name) 
{ 
    global $sess_save_path; 

    $sess_save_path = $save_path; 
    return(true); 
} 

function lazysess_close() 
{ 
    return(true); 
} 

function lazysess_read($id) 
{ 
    // Write and Close handlers are called after destructing objects 
    // since PHP 5.0.5. 
    // Thus destructors can use sessions but session handler can't use objects. 
    // So we are moving session closure before destructing objects. 
    register_shutdown_function('session_write_close'); 

    // Handle the case of first time visitors and clients that don't store cookies (eg. web crawlers). 
    if (!isset($_COOKIE[session_name()])) { 
     return ''; 
    } 

    // Continue with reading. 
    global $sess_save_path; 

    $sess_file = "$sess_save_path/sess_$id"; 
    return (string) @file_get_contents($sess_file); 
} 

function lazysess_write($id, $sess_data) 
{ 
    // If saving of session data is disabled, or if a new empty anonymous session 
    // has been started, do nothing. This keeps anonymous users, including 
    // crawlers, out of the session table, unless they actually have something 
    // stored in $_SESSION. 
    if (empty($_COOKIE[session_name()]) && empty($sess_data)) { 

     // Ensure that the client doesn't store the session cookie as it is worthless 
     lazysess_remove_session_cookie_header(); 

     return TRUE; 
    } 

    // Continue with storage 
    global $sess_save_path; 

    $sess_file = "$sess_save_path/sess_$id"; 
    if ($fp = @fopen($sess_file, "w")) { 
     $return = fwrite($fp, $sess_data); 
     fclose($fp); 
     return $return; 
    } else { 
     return(false); 
    } 

} 

function lazysess_destroy($id) 
{ 
    // If the session ID being destroyed is the one of the current user, 
    // clean-up his/her session data and cookie. 
    if ($id == session_id()) { 
     global $user; 

     // Reset $_SESSION and $user to prevent a new session from being started 
     // in drupal_session_commit() 
     $_SESSION = array(); 

     // Unset the session cookie. 
     lazysess_set_delete_cookie_header(); 
     if (isset($_COOKIE[session_name()])) { 
      unset($_COOKIE[session_name()]); 
     } 
    } 


    // Continue with destruction 
    global $sess_save_path; 

    $sess_file = "$sess_save_path/sess_$id"; 
    return(@unlink($sess_file)); 
} 

function lazysess_gc($maxlifetime) 
{ 
    global $sess_save_path; 

    foreach (glob("$sess_save_path/sess_*") as $filename) { 
     if (filemtime($filename) + $maxlifetime < time()) { 
      @unlink($filename); 
     } 
    } 
    return true; 
} 

/********************************************************* 
* Helper functions 
*********************************************************/ 

function lazysess_set_delete_cookie_header() { 
    $params = session_get_cookie_params(); 

    if (version_compare(PHP_VERSION, '5.2.0') === 1) { 
     setcookie(session_name(), '', $_SERVER['REQUEST_TIME'] - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']); 
    } 
    else { 
     setcookie(session_name(), '', $_SERVER['REQUEST_TIME'] - 3600, $params['path'], $params['domain'], $params['secure']);   
    } 
} 

function lazysess_remove_session_cookie_header() { 
    // Note: this implementation will blow away all Set-Cookie headers, not just 
    // those for the session cookie. If your app uses other cookies, reimplement 
    // this function. 
    header('Set-Cookie:', true); 
} 

/********************************************************* 
* Register the save handlers 
*********************************************************/ 

session_set_save_handler('lazysess_open', 'lazysess_close', 'lazysess_read', 'lazysess_write', 'lazysess_destroy', 'lazysess_gc'); 

雖然這種解決方案可行,幾乎都是透明的應用程序,包括它,它需要重寫整個會話存儲機制,而不是依賴於內置的存儲機制與交換機保存或不。

+0

注意:由於這個lib完全不使用鎖,因此併發請求可能會覆蓋對方的內容。 – staabm 2014-09-30 08:32:02

6

那麼,一個選擇是使用會話類來啓動/停止/存儲會話中的數據。所以,你可以這樣做:

class Session implements ArrayAccess { 
    protected $closed = false; 
    protected $data = array(); 
    protected $name = 'mySessionName'; 
    protected $started = false; 

    protected function __construct() { 
     if (isset($_COOKIE[$this->name])) $this->start(); 
     $this->data = $_SESSION; 
    } 

    public static function initialize() { 
     if (is_object($_SESSION)) return $_SESSION; 
     $_SESSION = new Session(); 
     register_shutdown_function(array($_SESSION, 'close')); 
     return $_SESSION; 
    } 

    public function close() { 
     if ($this->closed) return false; 
     if (!$this->started) { 
      $_SESSION = array(); 
     } else { 
      $_SESSION = $this->data; 
     } 
     session_write_close(); 
     $this->started = false; 
     $this->closed = true; 
    } 

    public function offsetExists($offset) { 
     return isset($this->data[$offset]); 
    } 

    public function offsetGet($offset) { 
     if (!isset($this->data[$offset])) { 
      throw new OutOfBoundsException('Key does not exist'); 
     } 
     return $this->data[$offset]; 
    } 

    public function offsetSet($offset, $value) { 
     $this->set($offset, $value); 
    } 

    public function offsetUnset($offset) { 
     if (isset($this->data[$offset])) unset($this->data[$offset]); 
    } 

    public function set($key, $value) { 
     if (!$this->started) $this->start(); 
     $this->data[$key] = $value; 
    } 

    public function start() { 
     session_name($this->name); 
     session_start(); 
     $this->started = true; 
    } 
} 

要使用,在你的腳本調用Session::initialize()的開始。它將用對象替換$ _SESSION,並設置延遲加載。之後,你可以做

$_SESSION['user_id'] = 1; 

如果會話沒有啓動,這將是,與USER_ID鍵將設置爲1,如果在任何時候你想關閉(提交)會議,就致電$_SESSION->close()

您可能想要添加更多的會話管理功能(例如destroy,regenerate_id,更改會話名稱的能力等),但是這應該實現您之後的基本功能......

這不是一個save_handler,它只是一個管理會話的類。如果你真的想,你可以在類中實現ArrayAccess,並在構造中用該類替換$ _SESSION(這樣做的好處是,遺留代碼仍然可以像以前一樣使用會話,而不需要調用$session->setData())。唯一的缺點是,我不確定PHP使用的序列化例程是否可以正常工作(您需要將數組放回到$ _SESSION中...可能與register_shutdown_function() ...

+0

這仍然是最值得推薦的解決方案嗎?我發現懶惰的會議很少,這讓我覺得他是另一種解決方案。 – 2012-02-29 20:28:23

+0

不要將其他內容分配給'$ _SESSION'超全局。 PHP負責處理該變量,並且如果會話狀態更改該變量get的set/unset/lost,甚至將PHP變爲涅ana。就像你不應該做'$ _SESSION = array()'你不應該做'$ _SESSION = new XYZ()'。相反,通過類。 – hakre 2012-08-01 08:23:56

+0

而不是'new Session()'使用'new self()',你可以將類重命名爲 – staabm 2014-10-14 15:56:34

0

我創造的概念在這裏慵懶的會議證明:

  • 它採用了原生的PHP會話處理程序,並_SESSION陣列
  • 如果一個cookie被髮送或
  • 它開始它只是啓動會議會話如果事情已經加入了$ _SESSION陣列
  • ,如果一個會話開始和$ _SESSION是空刪除會話

將在未來幾天進行擴展:

https://github.com/s0enke/php-lazy-session