【问题标题】:How should I implement lazy session creation in PHP?我应该如何在 PHP 中实现惰性会话创建?
【发布时间】:2011-03-01 04:50:54
【问题描述】:

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

我的目标是实现与 Drupal 7 和 Pressflow 类似的惰性会话创建行为,其中不存储会话(或发送会话 cookie 标头),除非在应用程序执行期间将数据添加到 $_SESSION 数组。此行为的目的是允许反向代理(例如 Varnish)缓存和服务匿名流量,同时让经过身份验证的请求通过 Apache/PHP。 Varnish(或其他代理服务器)被配置为通过任何不带 cookie 的请求,正确假设如果存在 cookie,则请求是针对特定客户端的。

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

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

【问题讨论】:

标签: php session


【解决方案1】:

嗯,一种选择是使用会话类在会话中启动/停止/存储数据。因此,您可以执行以下操作:

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()

您可能希望添加更多会话管理功能(例如销毁、regenerate_id、更改会话名称的能力等),但这应该实现您所追求的基本功能...

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

【讨论】:

  • 这仍然是最推荐的解决方案吗?我发现关于惰性会话的信息太少了,这让我认为这是另一种解决方案。
  • 不要为$_SESSION 超全局分配其他内容。 PHP 负责处理该变量,如果会话状态更改该变量的设置/取消设置/丢失,甚至 PHP 进入必杀技。就像你不应该做$_SESSION = array() 你不应该做$_SESSION = new XYZ()。而是传递课程。
  • 而不是 new Session() 使用 new self() 并且您可以将课程重命名为您喜欢的任何名称
  • 以上代码没有设置started属性,所以多次调用session_start()。
  • 重要提示:当用这样的对象/类替换 $_SESSION 时,您需要确保您没有使用数组期望函数的代码,例如 array_push($_SESSION, 'abc') 等,因为不支持非数组(或实现 ArrayAccess 的类)
【解决方案2】:

我为此问题开发了一个working solution,它使用session_set_save_handler() 和一组自定义会话存储方法,这些方法在写出会话数据之前检查$_SESSION 数组中的内容。如果没有要为会话写入的数据,则使用header('Set-Cookie:', true); 来防止在响应中发送 PHP 的 session-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');

虽然此解决方案有效并且对包括它在内的应用程序大多是透明的,但它需要重写整个会话存储机制,而不是依赖于内置存储机制以及是否保存的开关。

【讨论】:

  • 注意:由于这个库根本不使用锁定,并发请求可能会覆盖彼此的内容。
【解决方案3】:

我在这里创建了一个惰性会话概念证明:

  • 它使用原生 php 会话处理程序和 _SESSION 数组
  • 只有在发送 cookie 时才会启动会话或
  • 如果在 $_SESSION 数组中添加了某些内容,它将启动会话
  • 如果会话已启动且 $_SESSION 为空,它将删除会话

将在接下来的几天内延长它:

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

【讨论】:

    【解决方案4】:

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

    【讨论】:

      猜你喜欢
      • 2011-03-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多