How should I implement lazy session creation in PHP?

时间:2021-10-22 06:55:39

By default, PHP's session handling mechanisms set a session cookie header and store a session even if there is no data in the session. If no data is set in the session then I don't want a Set-Cookie header sent to the client in the response and I don't want an empty session record stored on the server. If data is added to $_SESSION, then the normal behavior should continue.

默认情况下,即使会话中没有数据,PHP的会话处理机制也会设置会话cookie标头并存储会话。如果会话中没有设置数据,那么我不希望在响应中向客户端发送Set-Cookie标头,并且我不希望在服务器上存储空的会话记录。如果将数据添加到$ _SESSION,则应继续正常行为。

My goal is to implement lazy session creation behavior of the sort that Drupal 7 and Pressflow where no session is stored (or session cookie header sent) unless data is added to the $_SESSION array during application execution. The point of this behavior is to allow reverse proxies such as Varnish to cache and serve anonymous traffic while letting authenticated requests pass through to Apache/PHP. Varnish (or another proxy-server) is configured to pass through any requests without cookies, assuming correctly that if a cookie exists then the request is for a particular client.

我的目标是实现Drupal 7和Pressflow排序的延迟会话创建行为,其中没有存储会话(或发送会话cookie标头),除非在应用程序执行期间将数据添加到$ _SESSION数组。这种行为的关键是允许反向代理(如Varnish)缓存并提供匿名流量,同时让经过身份验证的请求传递给Apache / PHP。 Varnish(或其他代理服务器)被配置为在没有cookie的情况下传递任何请求,假设正确地假设存在cookie,则该请求是针对特定客户端的。

I have ported the session handling code from Pressflow that uses session_set_save_handler() and overrides the implementation of session_write() to check for data in the $_SESSION array before saving and will write this up as library and add an answer here if this is the best/only route to take.

我已经从Pressflow移植会话处理代码,使用session_set_save_handler()并覆盖session_write()的实现以在保存之前检查$ _SESSION数组中的数据,并将其写为库并在此处添加答案,如果这是最好的/只有路线。

My Question: While I can implement a fully custom session_set_save_handler() system, is there an easier way to get this lazy session creation behavior in a relatively generic way that would be transparent to most applications?

我的问题:虽然我可以实现一个完全自定义的session_set_save_handler()系统,是否有一种更简单的方法来以相对通用的方式获得这种延迟会话创建行为,这对大多数应用程序是透明的?

4 个解决方案

#1


6  

Well, one option would be to use a session class to start/stop/store data in the session. So, you could do something like:

那么,一种选择是使用会话类来启动/停止/存储会话中的数据。所以,你可以这样做:

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;
    }
}

To use, at the start of your script call Session::initialize(). It will replace $_SESSION with the object, and setup the lazy loading. Afterward, you can just do

要使用,在脚本开头调用Session :: initialize()。它将用对象替换$ _SESSION,并设置延迟加载。之后,你可以做到

$_SESSION['user_id'] = 1;

If the session isn't started, it will be, and the user_id key would be set to 1. If at any point you wanted to close (commit) the session, just call $_SESSION->close().

如果会话未启动,则会是,并且user_id键将设置为1.如果您想要关闭(提交)会话,只需调用$ _SESSION-> close()。

You'll probably want to add some more session management functions (such as destroy, regenerate_id, the ability to change the name of the session, etc), but this should implement the basic functionality you're after...

您可能希望添加更多会话管理功能(例如destroy,regenerate_id,更改会话名称的能力等),但这应该实现您之后的基本功能......

It's not a save_handler, it's just a class to manage your sessions. If you really wanted to, you could implement ArrayAccess in the class, and on construct replace $_SESSION with that class (The benefit of doing that, is that way legacy code can still use session as they used to without calling $session->setData()). The only downside is that I'm not sure if the serialization routine that PHP uses would work properly (You'd need to put back the array into $_SESSION at some point... Probably with a register_shutdown_function()...

它不是save_handler,它只是一个管理会话的类。如果你真的想要,你可以在类中实现ArrayAccess,并在构造中用该类替换$ _SESSION(这样做的好处是遗留代码仍然可以像以前一样使用session而不调用$ session-> setData ())。唯一的缺点是我不确定PHP使用的序列化例程是否会正常工作(你需要在某个时候将数组放回$ _SESSION ......可能有一个register_shutdown_function()...

#2


3  

I have developed a working solution to this problem that uses session_set_save_handler() and a set of custom session storage methods that check for content in the $_SESSION array before writing out session data. If there is no data to write for the session, then header('Set-Cookie:', true); is used to prevent PHP's session-cookie from being sent in the response.

我已经开发了一个解决此问题的工作解决方案,它使用session_set_save_handler()和一组自定义会话存储方法,在写出会话数据之前检查$ _SESSION数组中的内容。如果没有要为会话写入的数据,则标题('Set-Cookie:',true);用于防止PHP的会话cookie在响应中发送。

The latest version of this code as well as documentation and examples are available on GitHub. In the code below, the important functions that make this work are lazysess_read($id) and lazysess_write($id, $sess_data).

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');

While this solution works and is mostly transparent to applications including it, it requires rewriting the entire session-storage mechanism rather than relying on the built-in storage mechanisms with a switch to save or not.

虽然这种解决方案适用并且对包括它在内的应用程序大多是透明的,但它需要重写整个会话存储机制,而不是依赖内置存储机制和交换机来保存或不保存。

#3


0  

I created a lazy session proof of concept here:

我在这里创建了一个懒惰的会话证明:

  • it uses the native php session handler and _SESSION array
  • 它使用本机php会话处理程序和_SESSION数组

  • it only starts the session if a cookie has been sent or
  • 它只会在发送cookie时启动会话或

  • it starts the session if something has been added the $_SESSION array
  • 如果添加了$ _SESSION数组,它会启动会话

  • it removes the session if a session is started and $_SESSION is empty
  • 如果会话已启动且$ _SESSION为空,则会删除会话

Will extend it in the next days:

将在接下来的几天内扩展它:

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

#4


0  

this topic is under discussion for a future php version https://wiki.php.net/rfc/session-read_only-lazy_write

这个主题正在讨论未来的php版本https://wiki.php.net/rfc/session-read_only-lazy_write

#1


6  

Well, one option would be to use a session class to start/stop/store data in the session. So, you could do something like:

那么,一种选择是使用会话类来启动/停止/存储会话中的数据。所以,你可以这样做:

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;
    }
}

To use, at the start of your script call Session::initialize(). It will replace $_SESSION with the object, and setup the lazy loading. Afterward, you can just do

要使用,在脚本开头调用Session :: initialize()。它将用对象替换$ _SESSION,并设置延迟加载。之后,你可以做到

$_SESSION['user_id'] = 1;

If the session isn't started, it will be, and the user_id key would be set to 1. If at any point you wanted to close (commit) the session, just call $_SESSION->close().

如果会话未启动,则会是,并且user_id键将设置为1.如果您想要关闭(提交)会话,只需调用$ _SESSION-> close()。

You'll probably want to add some more session management functions (such as destroy, regenerate_id, the ability to change the name of the session, etc), but this should implement the basic functionality you're after...

您可能希望添加更多会话管理功能(例如destroy,regenerate_id,更改会话名称的能力等),但这应该实现您之后的基本功能......

It's not a save_handler, it's just a class to manage your sessions. If you really wanted to, you could implement ArrayAccess in the class, and on construct replace $_SESSION with that class (The benefit of doing that, is that way legacy code can still use session as they used to without calling $session->setData()). The only downside is that I'm not sure if the serialization routine that PHP uses would work properly (You'd need to put back the array into $_SESSION at some point... Probably with a register_shutdown_function()...

它不是save_handler,它只是一个管理会话的类。如果你真的想要,你可以在类中实现ArrayAccess,并在构造中用该类替换$ _SESSION(这样做的好处是遗留代码仍然可以像以前一样使用session而不调用$ session-> setData ())。唯一的缺点是我不确定PHP使用的序列化例程是否会正常工作(你需要在某个时候将数组放回$ _SESSION ......可能有一个register_shutdown_function()...

#2


3  

I have developed a working solution to this problem that uses session_set_save_handler() and a set of custom session storage methods that check for content in the $_SESSION array before writing out session data. If there is no data to write for the session, then header('Set-Cookie:', true); is used to prevent PHP's session-cookie from being sent in the response.

我已经开发了一个解决此问题的工作解决方案,它使用session_set_save_handler()和一组自定义会话存储方法,在写出会话数据之前检查$ _SESSION数组中的内容。如果没有要为会话写入的数据,则标题('Set-Cookie:',true);用于防止PHP的会话cookie在响应中发送。

The latest version of this code as well as documentation and examples are available on GitHub. In the code below, the important functions that make this work are lazysess_read($id) and lazysess_write($id, $sess_data).

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');

While this solution works and is mostly transparent to applications including it, it requires rewriting the entire session-storage mechanism rather than relying on the built-in storage mechanisms with a switch to save or not.

虽然这种解决方案适用并且对包括它在内的应用程序大多是透明的,但它需要重写整个会话存储机制,而不是依赖内置存储机制和交换机来保存或不保存。

#3


0  

I created a lazy session proof of concept here:

我在这里创建了一个懒惰的会话证明:

  • it uses the native php session handler and _SESSION array
  • 它使用本机php会话处理程序和_SESSION数组

  • it only starts the session if a cookie has been sent or
  • 它只会在发送cookie时启动会话或

  • it starts the session if something has been added the $_SESSION array
  • 如果添加了$ _SESSION数组,它会启动会话

  • it removes the session if a session is started and $_SESSION is empty
  • 如果会话已启动且$ _SESSION为空,则会删除会话

Will extend it in the next days:

将在接下来的几天内扩展它:

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

#4


0  

this topic is under discussion for a future php version https://wiki.php.net/rfc/session-read_only-lazy_write

这个主题正在讨论未来的php版本https://wiki.php.net/rfc/session-read_only-lazy_write