代码之家  ›  专栏  ›  技术社区  ›  Adam Franco

我应该如何在PHP中实现延迟会话创建?

  •  13
  • Adam Franco  · 技术社区  · 14 年前

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

    我的目标是实现Drupal7和 Pressflow 除非将数据添加到 $会话 应用程序执行期间的数组。这种行为的要点是允许反向代理,例如 Varnish 缓存和服务匿名通信,同时让经过身份验证的请求通过apache/php。Varnish(或另一个代理服务器)被配置为在没有cookie的情况下传递任何请求,正确地假设如果存在cookie,那么请求是针对特定客户机的。

    我从PressFlow移植了会话处理代码 session_set_save_handler() 并重写 session_write() 检查中的数据 $会话 在保存之前数组,并将其写为库,如果这是最佳/唯一的路由,请在此处添加答案。

    我的问题: 我可以实现完全自定义 会话设置保存处理程序() 系统,是否有一种更容易的方法以一种相对通用的方式获得这种懒惰的会话创建行为 透明的 对于大多数应用程序?

    4 回复  |  直到 10 年前
        1
  •  6
  •   ircmaxell    10 年前

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

    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['user_id'] = 1;
    

    如果会话没有启动,它将启动,并且用户ID密钥将设置为1。如果在任何时候您想关闭(提交)会话,只需调用 $_SESSION->close() .

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

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

        2
  •  3
  •   Adam Franco    14 年前

    我开发了一个 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');
    

    虽然这个解决方案可以工作,并且对包括它在内的应用程序基本上是透明的,但是它需要重写整个会话存储机制,而不是依赖内置存储机制和一个要保存或不保存的开关。

        3
  •  0
  •   s0enke    13 年前

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

    • 它使用本机PHP会话处理程序和会话数组
    • 仅当发送了cookie或
    • 如果添加了某些内容,它将启动会话。
    • 如果会话已启动且$\u会话为空,则它将删除会话。

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

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

        4
  •  0
  •   staabm    10 年前

    此主题正在讨论将来的PHP版本 https://wiki.php.net/rfc/session-read_only-lazy_write