Как мне реализовать создание ленивых сессий в PHP? - PullRequest
13 голосов
/ 08 июня 2010

По умолчанию механизмы обработки сеансов 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(), существует ли более простой способ получить это ленивое поведение при создании сеанса относительно общим способом, который будет прозрачным для большинства приложений?

Ответы [ 4 ]

6 голосов
/ 08 июня 2010

Ну, одним из вариантов будет использование класса сеанса для запуска / остановки / сохранения данных в сеансе.Таким образом, вы можете сделать что-то вроде:

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

Возможно, вы захотите добавить еще несколько функций управления сеансом (например, уничтожить, регенерат_ид, возможность изменить имя сеанса и т. Д.), Но это должно реализовать основные функциональные возможности, которые вам нужны...

Это не save_handler, это просто класс для управления вашими сессиями.Если вы действительно этого хотите, вы можете реализовать ArrayAccess в классе, и при построении заменить $ _SESSION на этот класс (Преимущество этого заключается в том, что старый код все еще может использовать сессию, как раньше, без вызова $session->setData()).Единственным недостатком является то, что я не уверен, что подпрограмма сериализации, которую использует PHP, будет работать должным образом (в какой-то момент вам нужно будет вернуть массив в $ _SESSION ... Вероятно, с register_shutdown_function() ...

3 голосов
/ 08 июня 2010

Я разработал рабочее решение для этой проблемы, которое использует session_set_save_handler() и набор пользовательских методов хранения сеансов, которые проверяют содержимое в массиве $_SESSION перед записью данных сеанса. Если для сеанса нет данных для записи, то header('Set-Cookie:', true); используется для предотвращения отправки файла cookie сеанса PHP в ответе.

Последняя версия этого кода, а также документация и примеры доступны на 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 голосов
/ 30 сентября 2014

обсуждается эта тема для будущей версии php https://wiki.php.net/rfc/session-read_only-lazy_write

0 голосов
/ 10 января 2012

Я создал ленивое сессионное доказательство концепции здесь:

  • он использует собственный обработчик сессии php и массив _SESSION
  • запускает сессию, только если был отправлен файл cookie или
  • запускает сеанс, если что-то добавлено в массив $ _SESSION
  • удаляет сеанс, если сеанс запущен, а $ _SESSION пусто

Продлит его в следующие дни:

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...