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方法把数据写入持久化介质中。

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