Laravel 的 HTTP 會話機制——Session

本篇從Session |《Laravel 5.5 中文文檔 5.5》| Laravel China 社區入手,結合laravel源碼分析HTTP會話機制。

背景知識

在php原生語言中,我們可以使用$_SESSION全局數組來操作session,並且默認情況下,PHP使用內置的文件來保存session在請求間的數據。並且,從PHP4開始,可以使用PHP: session_set_save_handler - Manual函數來設置自定義會話函數:

session_set_save_handler ( callable $open , callable $close , callable $read , callable $write , callable $destroy , callable $gc [, callable $create_sid [, callable $validate_sid [, callable $update_timestamp ]]] ) : bool

通過該函數,我們可以設置session的打開、關閉、讀、寫、銷燬、垃圾回收操作,還可以定義創建sessionId、驗證sessionId和更新session時間的規則。自PHP5.4開始,可以使用面向對象的方式來註冊自定義會話存儲函數:

session_set_save_handler ( object $sessionhandler [, bool $register_shutdown = TRUE ] ) : bool

使用面向對象的方式需要我們自定義一個實現PHP: SessionHandlerInterface - Manual接口的類:

SessionHandlerInterface {
	/* 方法 */
	abstract public close ( void ) : bool
	abstract public destroy ( string $session_id ) : bool
	abstract public gc ( int $maxlifetime ) : int
	abstract public open ( string $save_path , string $session_name ) : bool
	abstract public read ( string $session_id ) : string
	abstract public write ( string $session_id , string $session_data ) : bool
}

一種更好的方法是繼承PHP: SessionHandler - Manual類:

SessionHandler implements SessionHandlerInterface , SessionIdInterface {
	/* 方法 */
	public close ( void ) : bool
	public create_sid ( void ) : string
	public destroy ( string $session_id ) : bool
	public gc ( int $maxlifetime ) : int
	public open ( string $save_path , string $session_name ) : bool
	public read ( string $session_id ) : string
	public write ( string $session_id , string $session_data ) : bool
}

通過PHP語音提供的這種擴展方式,我們自己就可以方便的寫出支持文件、數據庫、Redis、Memcache等存儲介質的http會話存儲方式。在本篇文章結束後,可以發現laravel中session的持久化採用了這種模式,但會話的使用並不是那麼原生。

簡介

由於 HTTP 驅動的應用程序是無狀態的,Session 提供了一種在多個請求之間存儲有關用戶的信息的方法。Laravel 通過同一個可讀性強的 API 處理各種自帶的 Session 後臺驅動程序。支持諸如比較熱門的 Memcached、Redis 和開箱即用的數據庫等常見的後臺驅動程序。

對應於每一種存儲介質,laravel提供了對應的SessionHandler類。
SessionHandler

Session driver 的配置選項定義了每個請求存儲 Session 數據的位置。Laravel 自帶了幾個不錯且可開箱即用的驅動:

  • file - 將 Session 保存在 storage/framework/sessions 中。
  • cookie - Session 保存在安全加密的 Cookie 中。
  • database - Session 保存在關係型數據庫中。
  • memcached / redis - Sessions 保存在其中一個快速且基於緩存的存儲系統中。
  • array - Sessions 保存在 PHP 數組中,不會被持久化。
  • NullSessionHandler
    當配置session驅動爲array時,會使用NullSessionHandler類來存儲session。NullSessionHandler所有方法中只有一行代碼:return true;。所以使用array作爲session驅動,session中的數據不存持久化,只會在內存中保存,當php執行結束後,session中的數據會全部丟失。如果一些數據只需要在當前請求中使用,可以使用array驅動。
  • FileSessionHandler
    當配置session驅動爲file時,會使用FileSessionHandler類來存儲session。file驅動也被稱爲native驅動,PHP默認就是使用文件來保存session數據的。FileSessionHandler使用FileSystem來操作文件,openclose方法只是return truereadwrite方法執行session數據的加載和存儲。read方法以sessionId爲文件名創建session文件。destroy方法會刪除指定sessionId的文件,gc方法會刪除過期的session文件。`
  • CookieSessionHandler
    當配置session驅動爲cookie時,會使用CookieSessionHandler類來存儲session。CookieSessionHandler使用Cookie來把操作數據。openclose方法只是return trueread方法根據sessionIdRequest中的cookie中加載數據,write方法把session中的數據寫入cookie中,destroy方法會刪除指定sessionId的數據,gc方法直接return true,數據的過期清除由cookie處理。
  • CacheBasedSessionHandler
    當配置session驅動爲memcached/redis/apc時,會使用CacheBasedSessionHandler類存儲session。使用redis作爲session驅動時,使用config/session.php中配置的connection設置連接;使用memcached/apc作爲session驅動時,使用config/session.php中配置的store設置緩存的驅動(與config/cache.php中的stores對應)。CacheBasedSessionHandler使用laravel的Cache來存儲數據(緩存系統 |《Laravel 5.5 中文文檔 5.5》| Laravel China 社區)。openclose方法只是retun truereadwrite直接從cache中讀取、保存數據;destory調用cacheforget方法清除數據;
    gc方法直接return true,清除過期數據由cache自動執行。
  • DatabaseSessionHandler
    當配置session驅動爲database時,會使用DatabaseSessionHandler類存儲session。

使用Session

session使用方式

Laravel 中處理 Session 數據有兩種主要方法:全局輔助函數 session 和通過一個 Request 實例。

除了這兩種方法還可以使用Session Facade。

  • session輔助方法
if (! function_exists('session')) {
    /**
     * Get / set the specified session value.
     *
     * If an array is passed as the key, we will assume you want to set an array of values.
     *
     * @param  array|string  $key
     * @param  mixed  $default
     * @return mixed|\Illuminate\Session\Store|\Illuminate\Session\SessionManager
     */
    function session($key = null, $default = null)
    {
        if (is_null($key)) {
            return app('session');
        }

        if (is_array($key)) {
            return app('session')->put($key);
        }

        return app('session')->get($key, $default);
    }
}

可以發現session是通過app('session')獲取的。

  • 通過Request實例
<?php

namespace Illuminate\Http;

use Symfony\Component\HttpFoundation\Request as SymfonyRequest;

class Request extends SymfonyRequest implements Arrayable, ArrayAccess
{
	...

    /**
     * Get the session associated with the request.
     *
     * @return \Illuminate\Session\Store
     *
     * @throws \RuntimeException
     */
    public function session()
    {
        if (! $this->hasSession()) {
            throw new RuntimeException('Session store not set on request.');
        }

        return $this->getSession();
    }

	...
}
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <[email protected]>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\HttpFoundation;

class Request
{
	...

    /**
     * Gets the Session.
     *
     * @return SessionInterface|null The session
     */
    public function getSession()
    {
        $session = $this->session;
        if (!$session instanceof SessionInterface && null !== $session) {
            $this->setSession($session = $session());
        }

        if (null === $session) {
            @trigger_error(sprintf('Calling "%s()" when no session has been set is deprecated since Symfony 4.1 and will throw an exception in 5.0. Use "hasSession()" instead.', __METHOD__), E_USER_DEPRECATED);
            // throw new \BadMethodCallException('Session has not been set');
        }

        return $session;
    }

	...
}

從Request實例中通過session()獲取session。

session相關方法

session相關方法主要定義在Store類中:Illuminate\Session\Store | Laravel API

Session相關類

app('session')返回的類型是Illuminate\Session\SessionManager,session相關的操作都會直接作用於SessionManager類的實例上,session的大量方法在Store類中定義,通過SessionManager類代理。
Laravel-Session
上面類圖中列出了laravel中Session所涉及的幾個重要的類結構。

SessionManaer

該類主要根據配置創建相關驅動的SessionHandler和創建Session對象。

  • 獲取配置
	/**
	 * Get the session configuration.
	 *
	 * @return array
	 */
	public function getSessionConfig()
	{
	    return $this->app['config']['session'];
	}
  • 創建array驅動的Session
    /**
     * Create an instance of the "array" session driver.
     *
     * @return \Illuminate\Session\Store
     */
    protected function createArrayDriver()
    {
        return $this->buildSession(new NullSessionHandler);
    }
  • 創建cookie驅動的Session
    /**
     * Create an instance of the "array" session driver.
     *
     * @return \Illuminate\Session\Store
     */
    protected function createArrayDriver()
    {
        return $this->buildSession(new NullSessionHandler);
    }
  • 創建file驅動的Session
    /**
     * Create an instance of the file session driver.
     *
     * @return \Illuminate\Session\Store
     */
    protected function createFileDriver()
    {
        return $this->createNativeDriver();
    }
    
    /**
     * Create an instance of the file session driver.
     *
     * @return \Illuminate\Session\Store
     */
    protected function createNativeDriver()
    {
        $lifetime = $this->app['config']['session.lifetime'];

        return $this->buildSession(new FileSessionHandler(
            $this->app['files'], $this->app['config']['session.files'], $lifetime
        ));
    }
  • 創建database驅動的Session
    /**
     * Create an instance of the database session driver.
     *
     * @return \Illuminate\Session\Store
     */
    protected function createDatabaseDriver()
    {
        $table = $this->app['config']['session.table'];

        $lifetime = $this->app['config']['session.lifetime'];

        return $this->buildSession(new DatabaseSessionHandler(
            $this->getDatabaseConnection(), $table, $lifetime, $this->app
        ));
    }

    /**
     * Get the database connection for the database driver.
     *
     * @return \Illuminate\Database\Connection
     */
    protected function getDatabaseConnection()
    {
        $connection = $this->app['config']['session.connection'];

        return $this->app['db']->connection($connection);
    }
  • 創建apc/memcached/redis驅動的Session
    /**
     * Create an instance of the APC session driver.
     *
     * @return \Illuminate\Session\Store
     */
    protected function createApcDriver()
    {
        return $this->createCacheBased('apc');
    }

    /**
     * Create an instance of the Memcached session driver.
     *
     * @return \Illuminate\Session\Store
     */
    protected function createMemcachedDriver()
    {
        return $this->createCacheBased('memcached');
    }

    /**
     * Create an instance of the Redis session driver.
     *
     * @return \Illuminate\Session\Store
     */
    protected function createRedisDriver()
    {
        $handler = $this->createCacheHandler('redis');

        $handler->getCache()->getStore()->setConnection(
            $this->app['config']['session.connection']
        );

        return $this->buildSession($handler);
    }

    /**
     * Create an instance of a cache driven driver.
     *
     * @param  string  $driver
     * @return \Illuminate\Session\Store
     */
    protected function createCacheBased($driver)
    {
        return $this->buildSession($this->createCacheHandler($driver));
    }

    /**
     * Create the cache based session handler instance.
     *
     * @param  string  $driver
     * @return \Illuminate\Session\CacheBasedSessionHandler
     */
    protected function createCacheHandler($driver)
    {
        $store = $this->app['config']->get('session.store') ?: $driver;

        return new CacheBasedSessionHandler(
            clone $this->app['cache']->store($store),
            $this->app['config']['session.lifetime']
        );
    }
  • 創建Session
    /**
     * Build the session instance.
     *
     * @param  \SessionHandlerInterface  $handler
     * @return \Illuminate\Session\Store
     */
    protected function buildSession($handler)
    {
        if ($this->app['config']['session.encrypt']) {
            return $this->buildEncryptedSession($handler);
        }

        return new Store($this->app['config']['session.cookie'], $handler);
    }
  • 設置和獲取默認驅動
    /**
     * Get the default session driver name.
     *
     * @return string
     */
    public function getDefaultDriver()
    {
        return $this->app['config']['session.driver'];
    }

    /**
     * Set the default session driver name.
     *
     * @param  string  $name
     * @return void
     */
    public function setDefaultDriver($name)
    {
        $this->app['config']['session.driver'] = $name;
    }

通過SessionManager,可以知道session驅動的管理方式。可以通過setDefaultDriver方法修改Session使用的默認驅動。

Manager

ManagerSessionManager的父類。

  • Manager中有一個重要的方法:__call
    /**
     * Dynamically call the default driver instance.
     *
     * @param  string  $method
     * @param  array   $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        return $this->driver()->$method(...$parameters);
    }

    /**
     * Get a driver instance.
     *
     * @param  string  $driver
     * @return mixed
     */
    public function driver($driver = null)
    {
        $driver = $driver ?: $this->getDefaultDriver();

        // If the given driver has not been created before, we will create the instances
        // here and cache it so we can return it next time very quickly. If there is
        // already a driver created by this name, we'll just return that instance.
        if (! isset($this->drivers[$driver])) {
            $this->drivers[$driver] = $this->createDriver($driver);
        }

        return $this->drivers[$driver];
    }

	 /**
     * Create a new driver instance.
     *
     * @param  string  $driver
     * @return mixed
     *
     * @throws \InvalidArgumentException
     */
    protected function createDriver($driver)
    {
        // We'll check to see if a creator method exists for the given driver. If not we
        // will check for a custom driver creator, which allows developers to create
        // drivers using their own customized driver creator Closure to create it.
        if (isset($this->customCreators[$driver])) {
            return $this->callCustomCreator($driver);
        } else {
            $method = 'create'.Str::studly($driver).'Driver';

            if (method_exists($this, $method)) {
                return $this->$method();
            }
        }
        throw new InvalidArgumentException("Driver [$driver] not supported.");
    }

通過魔術方法_call,對SessionManager操作會委託給由buildSession方法創建的Store類。
driver方法會先在drivers數組中查找驅動,如果沒有再創建驅動。
createDriver方法會先在自定義的創建器數組customCreators數組中查找驅動,如果有就調用callCustomCreator方法創建用戶自定義的方法創建驅動;如果沒有,就根據驅動名稱調用相應驅動創建方法,這些方法在子類SessionManager中定義。

  • 自定義驅動
    自定義驅動需要使用extend方法添加驅動名稱到drivers數組和添加自定義驅動創建閉包函數到customCreators數組。
    /**
     * Register a custom driver creator Closure.
     *
     * @param  string    $driver
     * @param  \Closure  $callback
     * @return $this
     */
    public function extend($driver, Closure $callback)
    {
        $this->customCreators[$driver] = $callback;

        return $this;
    }

    /**
     * Call a custom driver creator.
     *
     * @param  string  $driver
     * @return mixed
     */
    protected function callCustomCreator($driver)
    {
        return $this->buildSession(parent::callCustomCreator($driver));
    }

自定義的驅動創建閉包函數需要返回一個實現SessionHandler接口的類的實例給buildSession類創建Session。我們在子類SessionManager中能找到該函數的調用。

	/**
     * Call a custom driver creator.
     *
     * @param  string  $driver
     * @return mixed
     */
    protected function callCustomCreator($driver)
    {
        return $this->customCreators[$driver]($this->app);
    }

Store

SessionManagerbuildSession方法中可以看到創建的Session是一個Store類的實例。對SessionManager上的調用會委託給Store類。Store實現了Session中聲明的接口。
Store

  • 加載session
    session開始時,會調用SessionHanderread方法把session數據從持久化介質上加載到內存中,以後的操作都會在內存屬性attributes數組中執行。
    /**
     * Start the session, reading the data from a handler.
     *
     * @return bool
     */
    public function start()
    {
        $this->loadSession();

        if (! $this->has('_token')) {
            $this->regenerateToken();
        }

        return $this->started = true;
    }

    /**
     * Load the session data from the handler.
     *
     * @return void
     */
    protected function loadSession()
    {
        $this->attributes = array_merge($this->attributes, $this->readFromHandler());
    }

    /**
     * Read the session data from the handler.
     *
     * @return array
     */
    protected function readFromHandler()
    {
        if ($data = $this->handler->read($this->getId())) {
            $data = @unserialize($this->prepareForUnserialize($data));

            if ($data !== false && ! is_null($data) && is_array($data)) {
                return $data;
            }
        }

        return [];
    }
  • 生成sessionId
    /**
     * Get a new, random session ID.
     *
     * @return string
     */
    protected function generateSessionId()
    {
        return Str::random(40);
    }
  • 重新生成sessionId
    /**
     * Generate a new session identifier.
     *
     * @param  bool  $destroy
     * @return bool
     */
    public function regenerate($destroy = false)
    {
        return $this->migrate($destroy);
    }

    /**
     * Generate a new session ID for the session.
     *
     * @param  bool  $destroy
     * @return bool
     */
    public function migrate($destroy = false)
    {
        if ($destroy) {
            $this->handler->destroy($this->getId());
        }

        $this->setExists(false);

        $this->setId($this->generateSessionId());

        return true;
    }
  • 獲取session
    /**
     * Get an item from the session.
     *
     * @param  string  $key
     * @param  mixed  $default
     * @return mixed
     */
    public function get($key, $default = null)
    {
        return Arr::get($this->attributes, $key, $default);
    }

    /**
     * Get the value of a given key and then forget it.
     *
     * @param  string  $key
     * @param  string  $default
     * @return mixed
     */
    public function pull($key, $default = null)
    {
        return Arr::pull($this->attributes, $key, $default);
    }
  • 保存session
    /**
     * Push a value onto a session array.
     *
     * @param  string  $key
     * @param  mixed   $value
     * @return void
     */
    public function push($key, $value)
    {
        $array = $this->get($key, []);

        $array[] = $value;

        $this->put($key, $array);
    }

    /**
     * Flash a key / value pair to the session.
     *
     * @param  string  $key
     * @param  mixed   $value
     * @return void
     */
    public function flash(string $key, $value = true)
    {
        $this->put($key, $value);

        $this->push('_flash.new', $key);

        $this->removeFromOldFlashData([$key]);
    }

    /**
     * Flash a key / value pair to the session for immediate use.
     *
     * @param  string $key
     * @param  mixed $value
     * @return void
     */
    public function now($key, $value)
    {
        $this->put($key, $value);

        $this->push('_flash.old', $key);
    }

    /**
     * Reflash all of the session flash data.
     *
     * @return void
     */
    public function reflash()
    {
        $this->mergeNewFlashes($this->get('_flash.old', []));

        $this->put('_flash.old', []);
    }

    /**
     * Reflash a subset of the current flash data.
     *
     * @param  array|mixed  $keys
     * @return void
     */
    public function keep($keys = null)
    {
        $this->mergeNewFlashes($keys = is_array($keys) ? $keys : func_get_args());

        $this->removeFromOldFlashData($keys);
    }
  • 清除session
    /**
     * Remove an item from the session, returning its value.
     *
     * @param  string  $key
     * @return mixed
     */
    public function remove($key)
    {
        return Arr::pull($this->attributes, $key);
    }

    /**
     * Remove one or many items from the session.
     *
     * @param  string|array  $keys
     * @return void
     */
    public function forget($keys)
    {
        Arr::forget($this->attributes, $keys);
    }

    /**
     * Remove all of the items from the session.
     *
     * @return void
     */
    public function flush()
    {
        $this->attributes = [];
    }
  • 保存session
    /**
     * Save the session data to storage.
     *
     * @return bool
     */
    public function save()
    {
        $this->ageFlashData();

        $this->handler->write($this->getId(), $this->prepareForStorage(
            serialize($this->attributes)
        ));

        $this->started = false;
    }

有兩個方法配合CookieSessionHandler使用

    /**
     * Determine if the session handler needs a request.
     *
     * @return bool
     */
    public function handlerNeedsRequest()
    {
        return $this->handler instanceof CookieSessionHandler;
    }

    /**
     * Set the request on the handler instance.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return void
     */
    public function setRequestOnHandler($request)
    {
        if ($this->handlerNeedsRequest()) {
            $this->handler->setRequest($request);
        }
    }

維護session

這上面過程中,並沒有使用session_set_save_handler來設置session的管理器,session的開啓、保存什麼時候執行呢?

SessionServiceProvider

SessionServiceProvider是一個服務器提供者類,session通過該類在laravel中進行註冊。

class SessionServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->registerSessionManager();

        $this->registerSessionDriver();

        $this->app->singleton(StartSession::class);
    }

    /**
     * Register the session manager instance.
     *
     * @return void
     */
    protected function registerSessionManager()
    {
        $this->app->singleton('session', function ($app) {
            return new SessionManager($app);
        });
    }

    /**
     * Register the session driver instance.
     *
     * @return void
     */
    protected function registerSessionDriver()
    {
        $this->app->singleton('session.store', function ($app) {
            // First, we will create the session manager which is responsible for the
            // creation of the various session drivers when they are needed by the
            // application instance, and will resolve them on a lazy load basis.
            return $app->make('session')->driver();
        });
    }
}

SessionServiceProvider中以單例模式註冊了SessionManagerStoreStartSession類。

StartSession

StartSession類是一箇中間件,在app/Http/Kernel.php中分配在web中間組中。
StartSession

  • handle方法
    handle方法是中間件重要的方法
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $this->sessionHandled = true;

        // If a session driver has been configured, we will need to start the session here
        // so that the data is ready for an application. Note that the Laravel sessions
        // do not make use of PHP "native" sessions in any way since they are crappy.
        if ($this->sessionConfigured()) {
            $request->setLaravelSession(
                $session = $this->startSession($request)
            );

            $this->collectGarbage($session);
        }

        $response = $next($request);

        // Again, if the session has been configured we will need to close out the session
        // so that the attributes may be persisted to some storage medium. We will also
        // add the session identifier cookie to the application response headers now.
        if ($this->sessionConfigured()) {
            $this->storeCurrentUrl($request, $session);

            $this->addCookieToResponse($response, $session);
        }

        return $response;
    }

    /**
     * Start the session for the given request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Contracts\Session\Session
     */
    protected function startSession(Request $request)
    {
        return tap($this->getSession($request), function ($session) use ($request) {
            $session->setRequestOnHandler($request);

            $session->start();
        });
    }

    /**
     * Remove the garbage from the session if necessary.
     *
     * @param  \Illuminate\Contracts\Session\Session  $session
     * @return void
     */
    protected function collectGarbage(Session $session)
    {
        $config = $this->manager->getSessionConfig();

        // Here we will see if this request hits the garbage collection lottery by hitting
        // the odds needed to perform garbage collection on any given request. If we do
        // hit it, we'll call this handler to let it delete all the expired sessions.
        if ($this->configHitsLottery($config)) {
            $session->getHandler()->gc($this->getSessionLifetimeInSeconds());
        }
    }

handle方法中,調用startSession方法開啓session,加載數據。調用collectGarbage方法多過期的session數據進行垃圾回收。

  • terminate方法
    terminate方法會在在響應發送到瀏覽器後自動調用。
    /**
     * Perform any final actions for the request lifecycle.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Symfony\Component\HttpFoundation\Response  $response
     * @return void
     */
    public function terminate($request, $response)
    {
        if ($this->sessionHandled && $this->sessionConfigured() && ! $this->usingCookieSessions()) {
            $this->manager->driver()->save();
        }
    }

terminate方法中調用Storesave方法把數據寫入持久化介質中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章