因爲HTTP最初是一個匿名、無狀態的請求/響應協議,服務器處理來自客戶端的請求而後向客戶端回送一條響應。現代Web應用程序爲了給用戶提供個性化的服務每每須要在請求中識別出用戶或者在用戶的多條請求之間共享數據。Session 提供了一種在多個請求之間存儲、共享有關用戶的信息的方法。Laravel
經過同一個可讀性強的 API 處理各類自帶的 Session 後臺驅動程序。php
Session支持的驅動:git
file
- 將 Session 保存在 storage/framework/sessions
中。cookie
- Session 保存在安全加密的 Cookie 中。database
- Session 保存在關係型數據庫中。memcached
/ redis
- Sessions 保存在其中一個快速且基於緩存的存儲系統中。array
- Sessions 保存在 PHP 數組中,不會被持久化。這篇文章咱們來詳細的看一下Laravel
中Session
服務的實現原理,Session
服務有哪些部分組成以及每部分的角色、它是什麼時候被註冊到服務容器的、請求是在什麼時候啓用session的以及如何爲session擴展驅動。github
在以前的不少文章裏都提到過,服務是經過服務提供器註冊到服務容器裏的,Laravel在啓動階段會依次執行config/app.php
裏providers
數組裏的服務提供器register
方法來註冊框架須要的服務,因此咱們很容易想到session服務也是在這個階段被註冊到服務容器裏的。redis
'providers' => [ /* * Laravel Framework Service Providers... */ ...... Illuminate\Session\SessionServiceProvider::class ...... ],
果然在providers
裏確實有SessionServiceProvider
咱們看一下它的源碼,看看session服務的註冊細節數據庫
namespace Illuminate\Session; use Illuminate\Support\ServiceProvider; use Illuminate\Session\Middleware\StartSession; 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
中一共註冊了三個服務:數組
session
服務,session服務解析出來後是一個SessionManager
對象,它的做用是建立session驅動器而且在須要時解析出驅動器(延遲加載),此外一切訪問、更新session數據的方法調用都是由它代理給對應的session驅動器來實現的。session.store
Session驅動器,Illuminate\Session\Store
的實例,Store類
實現了Illuminate\Contracts\Session\Session
契約向開發者提供了統一的接口來訪問Session數據,驅動器經過不一樣的SessionHandler
來訪問database
、redis
、memcache
等不一樣的存儲介質裏的session數據。StartSession::class
中間件,提供了在請求開始時打開Session,響應發送給客戶端前將session標示符寫入到Cookie中,此外做爲一個terminate中間件
在響應發送給客戶端後它在terminate()
方法中會將請求中對session數據的更新保存到存儲介質中去。上面已經說了SessionManager
是用來建立session驅動器的,它裏面定義了各類個樣的驅動器建立器(建立驅動器實例的方法) 經過它的源碼來看一下session驅動器是證實被建立出來的:緩存
<?php namespace Illuminate\Session; use Illuminate\Support\Manager; class SessionManager extends Manager { /** * 調用自定義驅動建立器 (經過Session::extend註冊的) * * @param string $driver * @return mixed */ protected function callCustomCreator($driver) { return $this->buildSession(parent::callCustomCreator($driver)); } /** * 建立數組類型的session驅動器(不會持久化) * * @return \Illuminate\Session\Store */ protected function createArrayDriver() { return $this->buildSession(new NullSessionHandler); } /** * 建立Cookie session驅動器 * * @return \Illuminate\Session\Store */ protected function createCookieDriver() { return $this->buildSession(new CookieSessionHandler( $this->app['cookie'], $this->app['config']['session.lifetime'] )); } /** * 建立文件session驅動器 * * @return \Illuminate\Session\Store */ protected function createFileDriver() { return $this->createNativeDriver(); } /** * 建立文件session驅動器 * * @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驅動器 * * @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); } /** * Create an instance of the APC session driver. * * @return \Illuminate\Session\Store */ protected function createApcDriver() { return $this->createCacheBased('apc'); } /** * 建立memcache session驅動器 * * @return \Illuminate\Session\Store */ protected function createMemcachedDriver() { return $this->createCacheBased('memcached'); } /** * 建立redis session驅動器 * * @return \Illuminate\Session\Store */ protected function createRedisDriver() { $handler = $this->createCacheHandler('redis'); $handler->getCache()->getStore()->setConnection( $this->app['config']['session.connection'] ); return $this->buildSession($handler); } /** * 建立基於Cache的session驅動器 (建立memcache、apc驅動器時都會調用這個方法) * * @param string $driver * @return \Illuminate\Session\Store */ protected function createCacheBased($driver) { return $this->buildSession($this->createCacheHandler($driver)); } /** * 建立基於Cache的session handler * * @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驅動器 * * @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); } /** * 構建加密的Session驅動器 * * @param \SessionHandlerInterface $handler * @return \Illuminate\Session\EncryptedStore */ protected function buildEncryptedSession($handler) { return new EncryptedStore( $this->app['config']['session.cookie'], $handler, $this->app['encrypter'] ); } /** * 獲取config/session.php裏的配置 * * @return array */ public function getSessionConfig() { return $this->app['config']['session']; } /** * 獲取配置裏的session驅動器名稱 * * @return string */ public function getDefaultDriver() { return $this->app['config']['session.driver']; } /** * 設置配置裏的session名稱 * * @param string $name * @return void */ public function setDefaultDriver($name) { $this->app['config']['session.driver'] = $name; } }
經過SessionManager
的源碼能夠看到驅動器對外提供了統一的訪問接口,而不一樣類型的驅動器之因此能訪問不一樣的存儲介質是驅動器是經過SessionHandler
來訪問存儲介質裏的數據的,而不一樣的SessionHandler
統一都實現了PHP
內建的SessionHandlerInterface
接口,因此驅動器可以經過統一的接口方法訪問到不一樣的session存儲介質裏的數據。安全
開發者使用Session
門面或者$request->session()
訪問Session數據都是經過session
服務即SessionManager
對象轉發給對應的驅動器方法的,在Illuminate\Session\Store
的源碼中咱們也可以看到Laravel
裏用到的session方法都定義在這裏。服務器
Session::get($key); Session::has($key); Session::put($key, $value); Session::pull($key); Session::flash($key, $value); Session::forget($key);
上面這些session方法都能在Illuminate\Session\Store
類裏找到具體的方法實現cookie
<?php namespace Illuminate\Session; use Closure; use Illuminate\Support\Arr; use Illuminate\Support\Str; use SessionHandlerInterface; use Illuminate\Contracts\Session\Session; class Store implements Session { /** * The session ID. * * @var string */ protected $id; /** * The session name. * * @var string */ protected $name; /** * The session attributes. * * @var array */ protected $attributes = []; /** * The session handler implementation. * * @var \SessionHandlerInterface */ protected $handler; /** * Session store started status. * * @var bool */ protected $started = false; /** * Create a new session instance. * * @param string $name * @param \SessionHandlerInterface $handler * @param string|null $id * @return void */ public function __construct($name, SessionHandlerInterface $handler, $id = null) { $this->setId($id); $this->name = $name; $this->handler = $handler; } /** * 開啓session, 經過session handler從存儲介質中讀出數據暫存在attributes屬性裏 * * @return bool */ public function start() { $this->loadSession(); if (! $this->has('_token')) { $this->regenerateToken(); } return $this->started = true; } /** * 經過session handler從存儲中加載session數據暫存到attributes屬性裏 * * @return void */ protected function loadSession() { $this->attributes = array_merge($this->attributes, $this->readFromHandler()); } /** * 經過handler從存儲中讀出session數據 * * @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 []; } /** * Prepare the raw string data from the session for unserialization. * * @param string $data * @return string */ protected function prepareForUnserialize($data) { return $data; } /** * 將session數據保存到存儲中 * * @return bool */ public function save() { $this->ageFlashData(); $this->handler->write($this->getId(), $this->prepareForStorage( serialize($this->attributes) )); $this->started = false; } /** * Checks if a key is present and not null. * * @param string|array $key * @return bool */ public function has($key) { return ! collect(is_array($key) ? $key : func_get_args())->contains(function ($key) { return is_null($this->get($key)); }); } /** * 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); } /** * Put a key / value pair or array of key / value pairs in the session. * * @param string|array $key * @param mixed $value * @return void */ public function put($key, $value = null) { if (! is_array($key)) { $key = [$key => $value]; } foreach ($key as $arrayKey => $arrayValue) { Arr::set($this->attributes, $arrayKey, $arrayValue); } } /** * 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]); } /** * 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 = []; } /** * Determine if the session has been started. * * @return bool */ public function isStarted() { return $this->started; } /** * Get the name of the session. * * @return string */ public function getName() { return $this->name; } /** * Set the name of the session. * * @param string $name * @return void */ public function setName($name) { $this->name = $name; } /** * Get the current session ID. * * @return string */ public function getId() { return $this->id; } /** * Set the session ID. * * @param string $id * @return void */ public function setId($id) { $this->id = $this->isValidId($id) ? $id : $this->generateSessionId(); } /** * Determine if this is a valid session ID. * * @param string $id * @return bool */ public function isValidId($id) { return is_string($id) && ctype_alnum($id) && strlen($id) === 40; } /** * Get a new, random session ID. * * @return string */ protected function generateSessionId() { return Str::random(40); } /** * Set the existence of the session on the handler if applicable. * * @param bool $value * @return void */ public function setExists($value) { if ($this->handler instanceof ExistenceAwareInterface) { $this->handler->setExists($value); } } /** * Get the CSRF token value. * * @return string */ public function token() { return $this->get('_token'); } /** * Regenerate the CSRF token value. * * @return void */ public function regenerateToken() { $this->put('_token', Str::random(40)); } }
因爲驅動器的源碼比較多,我只留下一些經常使用和方法,並對關鍵的方法作了註解,完整源碼能夠去看Illuminate\Session\Store
類的源碼。 經過Store
類的源碼咱們能夠發現:
_token
數據來作CSRF
防範。同時也會產生一些疑問,在平時開發時咱們並無主動的去開啓和保存session,數據是怎麼加載和持久化的?經過session在用戶的請求間共享數據是須要在客戶端cookie存儲一個session id
的,這個cookie又是在哪裏設置的?
上面的兩個問題給出的解決方案是最開始說的第三個服務StartSession
中間件
<?php namespace Illuminate\Session\Middleware; use Closure; use Illuminate\Http\Request; use Illuminate\Support\Carbon; use Illuminate\Session\SessionManager; use Illuminate\Contracts\Session\Session; use Illuminate\Session\CookieSessionHandler; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Response; class StartSession { /** * The session manager. * * @var \Illuminate\Session\SessionManager */ protected $manager; /** * Indicates if the session was handled for the current request. * * @var bool */ protected $sessionHandled = false; /** * Create a new session middleware. * * @param \Illuminate\Session\SessionManager $manager * @return void */ public function __construct(SessionManager $manager) { $this->manager = $manager; } /** * 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; } /** * 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(); } } /** * 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(); }); } /** * Add the session cookie to the application response. * * @param \Symfony\Component\HttpFoundation\Response $response * @param \Illuminate\Contracts\Session\Session $session * @return void */ protected function addCookieToResponse(Response $response, Session $session) { if ($this->usingCookieSessions()) { //將session數據保存到cookie中,cookie名是本條session數據的ID標識符 $this->manager->driver()->save(); } if ($this->sessionIsPersistent($config = $this->manager->getSessionConfig())) { //將本條session的ID標識符保存到cookie中,cookie名是session配置文件裏設置的cookie名 $response->headers->setCookie(new Cookie( $session->getName(), $session->getId(), $this->getCookieExpirationDate(), $config['path'], $config['domain'], $config['secure'] ?? false, $config['http_only'] ?? true, false, $config['same_site'] ?? null )); } } /** * Determine if the configured session driver is persistent. * * @param array|null $config * @return bool */ protected function sessionIsPersistent(array $config = null) { $config = $config ?: $this->manager->getSessionConfig(); return ! in_array($config['driver'], [null, 'array']); } /** * Determine if the session is using cookie sessions. * * @return bool */ protected function usingCookieSessions() { if ($this->sessionConfigured()) { return $this->manager->driver()->getHandler() instanceof CookieSessionHandler; } return false; } }
一樣的我只保留了最關鍵的代碼,能夠看到中間件在請求進來時會先進行session start
操做,而後在響應返回給客戶端前將session id
設置到了cookie響應頭裏面, cookie的名稱是由config/session.php
裏的cookie
配置項設置的,值是本條session的ID標識符。與此同時若是session驅動器用的是CookieSessionHandler
還會將session數據保存到cookie裏cookie的名字是本條session的ID標示符(呃, 有點繞,其實就是把存在redis
裏的那些session數據以ID爲cookie名存到cookie裏了, 值是JSON
格式化的session數據)。
最後在響應發送完後,在terminate
方法裏會判斷驅動器用的若是不是CookieSessionHandler
,那麼就調用一次$this->manager->driver()->save();
將session數據持久化到存儲中 (我如今尚未搞清楚爲何不統一在這裏進行持久化,可能看完Cookie服務的源碼就清楚了)。
關於添加自定義驅動,官方文檔給出了一個例子,MongoHandler
必須實現統一的SessionHandlerInterface
接口裏的方法:
<?php namespace App\Extensions; class MongoHandler implements SessionHandlerInterface { public function open($savePath, $sessionName) {} public function close() {} public function read($sessionId) {} public function write($sessionId, $data) {} public function destroy($sessionId) {} public function gc($lifetime) {} }
定義完驅動後在AppServiceProvider
裏註冊一下:
<?php namespace App\Providers; use App\Extensions\MongoSessionStore; use Illuminate\Support\Facades\Session; use Illuminate\Support\ServiceProvider; class SessionServiceProvider extends ServiceProvider { /** * 執行註冊後引導服務。 * * @return void */ public function boot() { Session::extend('mongo', function ($app) { // Return implementation of SessionHandlerInterface... return new MongoSessionStore; }); } }
這樣在用SessionManager
的driver
方法建立mongo
類型的驅動器的時候就會調用callCustomCreator
方法去建立mongo
類型的Session驅動器了。
本文已經收錄在系列文章Laravel源碼學習裏,歡迎訪問閱讀。