本篇文章給大家帶來的內容是關于laravel框架核心內容:session源碼的詳細分析 ,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。
Session 模塊源碼解析
由于HTTP最初是一個匿名、無狀態的請求/響應協議,服務器處理來自客戶端的請求然后向客戶端回送一條響應。現代Web應用程序為了給用戶提供個性化的服務往往需要在請求中識別出用戶或者在用戶的多條請求之間共享數據。Session 提供了一種在多個請求之間存儲、共享有關用戶的信息的方法。laravel 通過同一個可讀性強的 API 處理各種自帶的 Session 后臺驅動程序。
Session支持的驅動:
-
file – 將 Session 保存在 storage/framework/sessions 中。
-
cookie – Session 保存在安全加密的 Cookie 中。
-
database – Session 保存在關系型數據庫中。
-
memcached / redis – Sessions 保存在其中一個快速且基于緩存的存儲系統中。
-
array – Sessions 保存在 PHP 數組中,不會被持久化。
這篇文章我們來詳細的看一下Laravel中Session服務的實現原理,Session服務有哪些部分組成以及每部分的角色、它是何時被注冊到服務容器的、請求是在何時啟用session的以及如何為session擴展驅動。
注冊Session服務
在之前的很多文章里都提到過,服務是通過服務提供器注冊到服務容器里的,Laravel在啟動階段會依次執行config/app.php里providers數組里的服務提供器register方法來注冊框架需要的服務,所以我們很容易想到session服務也是在這個階段被注冊到服務容器里的。
'providers' => [ /* * Laravel Framework Service Providers... */ ...... IlluminateSessionSessionServiceProvider::class ...... ],
果真在providers里確實有SessionServiceProvider 我們看一下它的源碼,看看session服務的注冊細節
namespace IlluminateSession; use IlluminateSupportServiceProvider; use IlluminateSessionMiddlewareStartSession; 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驅動器,IlluminateSessionStore的實例,Store類實現了IlluminateContractsSessionSession契約向開發者提供了統一的接口來訪問Session數據,驅動器通過不同的SessionHandler來訪問database、redis、memcache等不同的存儲介質里的session數據。
-
StartSession::class 中間件,提供了在請求開始時打開Session,響應發送給客戶端前將session標示符寫入到Cookie中,此外作為一個terminate中間件在響應發送給客戶端后它在terminate()方法中會將請求中對session數據的更新保存到存儲介質中去。
創建Session驅動器
上面已經說了SessionManager是用來創建session驅動器的,它里面定義了各種個樣的驅動器創建器(創建驅動器實例的方法) 通過它的源碼來看一下session驅動器是證明被創建出來的:
<?php namespace IlluminateSession; use IlluminateSupportManager; class SessionManager extends Manager { /** * 調用自定義驅動創建器 (通過Session::extend注冊的) * * @param string $driver * @return mixed */ protected function callCustomCreator($driver) { return $this->buildSession(parent::callCustomCreator($driver)); } /** * 創建數組類型的session驅動器(不會持久化) * * @return IlluminateSessionStore */ protected function createArrayDriver() { return $this->buildSession(new NullSessionHandler); } /** * 創建Cookie session驅動器 * * @return IlluminateSessionStore */ protected function createCookieDriver() { return $this->buildSession(new CookieSessionHandler( $this->app['cookie'], $this->app['config']['session.lifetime'] )); } /** * 創建文件session驅動器 * * @return IlluminateSessionStore */ protected function createFileDriver() { return $this->createNativeDriver(); } /** * 創建文件session驅動器 * * @return IlluminateSessionStore */ 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 IlluminateSessionStore */ 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 IlluminateDatabaseConnection */ protected function getDatabaseConnection() { $connection = $this->app['config']['session.connection']; return $this->app['db']->connection($connection); } /** * Create an instance of the APC session driver. * * @return IlluminateSessionStore */ protected function createApcDriver() { return $this->createCacheBased('apc'); } /** * 創建memcache session驅動器 * * @return IlluminateSessionStore */ protected function createMemcachedDriver() { return $this->createCacheBased('memcached'); } /** * 創建redis session驅動器 * * @return IlluminateSessionStore */ 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 IlluminateSessionStore */ protected function createCacheBased($driver) { return $this->buildSession($this->createCacheHandler($driver)); } /** * 創建基于Cache的session handler * * @param string $driver * @return IlluminateSessionCacheBasedSessionHandler */ 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 IlluminateSessionStore */ 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 IlluminateSessionEncryptedStore */ 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 數據
開發者使用Session門面或者$request->session()訪問Session數據都是通過session服務即SessionManager對象轉發給對應的驅動器方法的,在IlluminateSessionStore的源碼中我們也能夠看到Laravel里用到的session方法都定義在這里。
Session::get($key); Session::has($key); Session::put($key, $value); Session::pull($key); Session::flash($key, $value); Session::forget($key);
上面這些session方法都能在IlluminateSessionStore類里找到具體的方法實現
<?php namespace IlluminateSession; use Closure; use IlluminateSupportArr; use IlluminateSupportStr; use SessionHandlerInterface; use IlluminateContractsSessionSession; 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)); } }
由于驅動器的源碼比較多,我只留下一些常用和方法,并對關鍵的方法做了注解,完整源碼可以去看IlluminateSessionStore類的源碼。 通過Store類的源碼我們可以發現:
-
每個session數據里都會有一個_token數據來做CSRF防范。
-
Session開啟后會將session數據從存儲中讀出暫存到attributes屬性。
-
驅動器提供給應用操作session數據的方法都是直接操作的attributes屬性里的數據。
同時也會產生一些疑問,在平時開發時我們并沒有主動的去開啟和保存session,數據是怎么加載和持久化的?通過session在用戶的請求間共享數據是需要在客戶端cookie存儲一個session id的,這個cookie又是在哪里設置的?
上面的兩個問題給出的解決方案是最開始說的第三個服務StartSession中間件
StartSession 中間件
<?php namespace IlluminateSessionMiddleware; use Closure; use IlluminateHttpRequest; use IlluminateSupportCarbon; use IlluminateSessionSessionManager; use IlluminateContractsSessionSession; use IlluminateSessionCookieSessionHandler; use SymfonyComponentHttpFoundationCookie; use SymfonyComponentHttpFoundationResponse; class StartSession { /** * The session manager. * * @var IlluminateSessionSessionManager */ protected $manager; /** * Indicates if the session was handled for the current request. * * @var bool */ protected $sessionHandled = false; /** * Create a new session middleware. * * @param IlluminateSessionSessionManager $manager * @return void */ public function __construct(SessionManager $manager) { $this->manager = $manager; } /** * Handle an incoming request. * * @param IlluminateHttpRequest $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 IlluminateHttpRequest $request * @param SymfonyComponentHttpFoundationResponse $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 IlluminateHttpRequest $request * @return IlluminateContractsSessionSession */ 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 SymfonyComponentHttpFoundationResponse $response * @param IlluminateContractsSessionSession $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 AppExtensions; 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 AppProviders; use AppExtensionsMongoSessionStore; use IlluminateSupportFacadesSession; use IlluminateSupportServiceProvider; 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驅動器了。
相關推薦: