前言
本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/lar...
在網頁開發中, session
具有重要的作用,它可以在多個請求中儲存使用者的資訊,用於識別使用者的身份資訊。laravel
為使用者提供了可讀性強的 API 處理各種自帶的 Session 後臺驅動程式。支援諸如比較熱門的 Memcached、Redis 和開箱即用的資料庫等常見的後臺驅動程式。本文將會在本篇文章中講述最常見的由 File
與 redis
驅動的 session
原始碼。
session 服務的註冊
與其他功能一樣,session
由自己的服務提供者在 container
內進行註冊:
class SessionServiceProvider extends ServiceProvider
{
public function register()
{
$this->registerSessionManager();
$this->registerSessionDriver();
$this->app->singleton(StartSession::class);
}
protected function registerSessionManager()
{
$this->app->singleton('session', function ($app) {
return new SessionManager($app);
});
}
protected function registerSessionDriver()
{
$this->app->singleton('session.store', function ($app) {
return $app->make('session')->driver();
});
}
}
可以看到 SessionManager
是整個 session
服務的介面類,一切對 session
的操作都是由這個類實現。session.store
是 session
服務的儲存驅動。
session 服務的啟動
session
服務是以中介軟體的形式啟動的,其中介軟體是 Illuminate\Session\Middleware\StartSession
:
public function handle($request, Closure $next)
{
$this->sessionHandled = true;
if ($this->sessionConfigured()) {
$request->setLaravelSession(
$session = $this->startSession($request)
);
$this->collectGarbage($session);
}
$response = $next($request);
if ($this->sessionConfigured()) {
$this->storeCurrentUrl($request, $session);
$this->addCookieToResponse($response, $session);
}
return $response;
}
public function terminate($request, $response)
{
if ($this->sessionHandled && $this->sessionConfigured() && ! $this->usingCookieSessions()) {
$this->manager->driver()->save();
}
}
session
服務的中介軟體在 http
會話前與會話後都有處理。
在會話前,
laravel
試圖從cookies
中獲取sessionId
;- 利用
sessionId
讀取伺服器中的session
資料; - 將
session
物件存入request
中; session
垃圾回收
在會話後,
- 儲存當前的
url
作為session
的PreviousUrl
- 將當前的
session
存入瀏覽器cookies
中 - 儲存當前的
session
資料到儲存器驅動
startSession
startSession
函式進行了 session
的啟動工作:
public function __construct(SessionManager $manager)
{
$this->manager = $manager;
}
protected function startSession(Request $request)
{
return tap($this->getSession($request), function ($session) use ($request) {
$session->setRequestOnHandler($request);
$session->start();
});
}
public function getSession(Request $request)
{
return tap($this->manager->driver(), function ($session) use ($request) {
$session->setId($request->cookies->get($session->getName()));
});
}
session 的門面類 sessionManager
程式碼很簡潔,session
服務啟動的邏輯被包含在了 sessionManager
中,sessionManager
是 session
服務的門面類,負責 session
服務的驅動載入與資料操作。
首先我們先看看 SessionManager
:
namespace Illuminate\Session;
use Illuminate\Support\Manager;
class SessionManager extends Manager
{
}
SessionManager
繼承 Manager
類:
namespace Illuminate\Support;
abstract class Manager
{
public function driver($driver = null)
{
$driver = $driver ?: $this->getDefaultDriver();
if (! isset($this->drivers[$driver])) {
$this->drivers[$driver] = $this->createDriver($driver);
}
return $this->drivers[$driver];
}
}
當我們呼叫 driver
函式的時候,程式就開始為 session
服務載入驅動,例如對資料庫或者 redis
驅動,進行 連線
操作。
public function getDefaultDriver()
{
return $this->app['config']['session.driver'];
}
protected function createDriver($driver)
{
$method = 'create'.Str::studly($driver).'Driver';
if (isset($this->customCreators[$driver])) {
return $this->callCustomCreator($driver);
} elseif (method_exists($this, $method)) {
return $this->$method();
}
throw new InvalidArgumentException("Driver [$driver] not supported.");
}
session 驅動持久化類 SessionHandler
FileSessionHandler
這個類就是驅動,它繼承 SessionHandlerInterface
基類,任何對 session
的讀取、新增、刪除、更新等等操作最後都要通過這個驅動類進行持久化。
file
驅動:
file
驅動的核心是 Filesystem
,該類是 Ioc 容器建立的:
protected function createFileDriver()
{
return $this->createNativeDriver();
}
protected function createNativeDriver()
{
$lifetime = $this->app['config']['session.lifetime'];
return $this->buildSession(new FileSessionHandler(
$this->app['files'], $this->app['config']['session.files'], $lifetime
));
}
namespace Illuminate\Session;
class FileSessionHandler implements SessionHandlerInterface
{
public function __construct(Filesystem $files, $path, $minutes)
{
$this->path = $path;
$this->files = $files;
$this->minutes = $minutes;
}
}
- redis 驅動
redis
驅動並不是直接建立 redis
,而是利用了 laravel
的快取 cache
系統建立 redis
驅動,然後對 redis
驅動進行連線操作:
protected function createRedisDriver()
{
$handler = $this->createCacheHandler('redis');
$handler->getCache()->getStore()->setConnection(
$this->app['config']['session.connection']
);
return $this->buildSession($handler);
}
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']
);
}
class CacheBasedSessionHandler implements SessionHandlerInterface
{
public function __construct(CacheContract $cache, $minutes)
{
$this->cache = $cache;
$this->minutes = $minutes;
}
}
session 資料操作類
buildSession
函式將會返回 Store
類,這個 Store
類實際上 session
服務資料操作的實質類,任何對 session
資料的操作實際上呼叫的都是 Store
類:
protected function buildSession($handler)
{
if ($this->app['config']['session.encrypt']) {
return $this->buildEncryptedSession($handler);
} else {
return new Store($this->app['config']['session.cookie'], $handler);
}
}
protected function buildEncryptedSession($handler)
{
return new EncryptedStore(
$this->app['config']['session.cookie'], $handler, $this->app['encrypter']
);
}
public function __call($method, $parameters)
{
return $this->driver()->$method(...$parameters);
}
如果需要對 session
進行加密,那麼就會建立一個 EncryptedStore
類,該類繼承 Store
類。
setId
session
驅動建立之後,就要進行 sessionId
的設定,如果 cookie
中存在 sessionId
,我們就會從中獲取,否則我們就需要重新生成新的 sessionId
public function setId($id)
{
$this->id = $this->isValidId($id) ? $id : $this->generateSessionId();
}
public function isValidId($id)
{
return is_string($id) && ctype_alnum($id) && strlen($id) === 40;
}
protected function generateSessionId()
{
return Str::random(40);
}
session--start
一切準備就緒後,我們就要啟動 session
,如果當前請求存在未過期 session
,那麼就要利用 session
驅動將資料讀取出來:
public function start()
{
$this->loadSession();
if (! $this->has('_token')) {
$this->regenerateToken();
}
return $this->started = true;
}
protected function loadSession()
{
$this->attributes = array_merge($this->attributes, $this->readFromHandler());
}
readFromHandler
函式就是讀取 session
的過程:
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 [];
}
- 未加密 session 資料的載入
對於未加密的 session
來說,prepareForUnserialize
直接返回了資料:
protected function prepareForUnserialize($data)
{
return $data;
}
- 加密 session 資料
protected function prepareForUnserialize($data)
{
try {
return $this->encrypter->decrypt($data);
} catch (DecryptException $e) {
return serialize([]);
}
}
- file 驅動
public function read($sessionId)
{
if ($this->files->exists($path = $this->path.'/'.$sessionId)) {
if (filemtime($path) >= Carbon::now()->subMinutes($this->minutes)->getTimestamp()) {
return $this->files->get($path, true);
}
}
return '';
}
- redis 驅動
public function read($sessionId)
{
return $this->cache->get($sessionId, '');
}
session 垃圾回收
session
的垃圾回收用於隨機性地刪除舊 session
資料。由於某些驅動,例如 FileSessionHandler
, 程式不會定期刪除那些已經過時的 session
檔案,那麼 session
檔案一定會越來越多,所以我們就需要一種垃圾回收機制:
protected function collectGarbage(Session $session)
{
$config = $this->manager->getSessionConfig();
if ($this->configHitsLottery($config)) {
$session->getHandler()->gc($this->getSessionLifetimeInSeconds());
}
}
protected function configHitsLottery(array $config)
{
return random_int(1, $config['lottery'][1]) <= $config['lottery'][0];
}
configHitsLottery
函式就是判斷當前是否被隨機要進行垃圾回收任務。這種隨機性概率由 lottery
來設定。
FileSessionHandler
的垃圾回收:
public function gc($lifetime)
{
$files = Finder::create()
->in($this->path)
->files()
->ignoreDotFiles(true)
->date('<= now - '.$lifetime.' seconds');
foreach ($files as $file) {
$this->files->delete($file->getRealPath());
}
}
儲存前一頁
很多時候我們都需要從 session
中獲取前一頁的地址,例如使用者授權失敗就會返回上一頁等等情景。
protected function storeCurrentUrl(Request $request, $session)
{
if ($request->method() === 'GET' && $request->route() && ! $request->ajax()) {
$session->setPreviousUrl($request->fullUrl());
}
}
public function setPreviousUrl($url)
{
$this->put('_previous.url', $url);
}
中介軟體的結束
當請求結束時,會呼叫中介軟體的 terminate
函式,這裡程式會將新的 session
資料持久化到各個驅動器中:
public function terminate($request, $response)
{
if ($this->sessionHandled && $this->sessionConfigured() && ! $this->usingCookieSessions()) {
$this->manager->driver()->save();
}
}
session
的儲存:
public function save()
{
$this->ageFlashData();
$this->handler->write($this->getId(), $this->prepareForStorage(
serialize($this->attributes)
));
$this->started = false;
}
session
的儲存會刪除需要 flash
的快閃記憶體資料,也就是隻想用於下一次請求的資料:
public function ageFlashData()
{
$this->forget($this->get('_flash.old', []));
$this->put('_flash.old', $this->get('_flash.new', []));
$this->put('_flash.new', []);
}
對於不加密的資料,儲存前的 prepareForStorage
不會對資料進行任何操作:
protected function prepareForStorage($data)
{
return $data;
}
對於加密的資料,則需要事先加密:
protected function prepareForStorage($data)
{
return $this->encrypter->encrypt($data);
}
session 資料操作
get 函式
當我們想要獲取 session
中的資料時,我們經常使用 get
方法
public function show(Request $request, $id)
{
$value = $request->session()->get('key');
//
}
get
方法首先會呼叫 sessionManager
的魔術方法:
public function __call($method, $parameters)
{
return $this->driver()->$method(...$parameters);
}
driver
函式會返回 Store
物件,呼叫 get
方法
public function get($key, $default = null)
{
return Arr::get($this->attributes, $key, $default);
}
我們從上一節知道,在 startSession
中介軟體啟動後,session
資料已經載入到了 store
物件中,因此獲取資料很簡單:
public function get($key, $default = null)
{
return Arr::get($this->attributes, $key, $default);
}
all 函式
all
函式可以取出所有的 session
資料
public function all()
{
return $this->attributes;
}
has 函式
要確定 Session 中是否存在某個值,可以使用 has
方法。如果該值存在且不為 null
,那麼 has
方法會返回 true
:
public function has($key)
{
return ! collect(is_array($key) ? $key : func_get_args())->contains(function ($key) {
return is_null($this->get($key));
});
}
exists 函式
要確定 Session
中是否存在某個值,即使其值為 null
,也可以使用 exists
方法。如果值存在,則 exists
方法返回 true
public function exists($key)
{
return ! collect(is_array($key) ? $key : func_get_args())->contains(function ($key) {
return ! Arr::exists($this->attributes, $key);
});
}
put 方法
要儲存資料到 Session
,可以使用 put
方法
public function put($key, $value = null)
{
if (! is_array($key)) {
$key = [$key => $value];
}
foreach ($key as $arrayKey => $arrayValue) {
Arr::set($this->attributes, $arrayKey, $arrayValue);
}
}
push 方法
push
方法可以將一個新的值新增到 Session
陣列內。
public function push($key, $value)
{
$array = $this->get($key, []);
$array[] = $value;
$this->put($key, $array);
}
remember 方法
remember
方法用於有即取,無即存的情況:
public function remember($key, Closure $callback)
{
if (! is_null($value = $this->get($key))) {
return $value;
}
return tap($callback(), function ($value) use ($key) {
$this->put($key, $value);
});
}
increment 方法
increment
方法用於增加某 session
資料的值:
public function increment($key, $amount = 1)
{
$this->put($key, $value = $this->get($key, 0) + $amount);
return $value;
}
decrement 方法
public function decrement($key, $amount = 1)
{
return $this->increment($key, $amount * -1);
}
pull 方法
pull
方法可以只用一條語句就從 Session
檢索並且刪除一個專案:
public function pull($key, $default = null)
{
return Arr::pull($this->attributes, $key, $default);
}
flash 快閃記憶體資料
有時候你僅想在下一個請求之前在 Session
中存入資料,你可以使用 flash
方法。使用這個方法儲存在 session
中的資料,只會保留到下個 HTTP
請求到來之前,然後就會被刪除。快閃記憶體資料主要用於短期的狀態訊息
public function flash($key, $value)
{
$this->put($key, $value);
$this->push('_flash.new', $key);
$this->removeFromOldFlashData([$key]);
}
protected function removeFromOldFlashData(array $keys)
{
$this->put('_flash.old', array_diff($this->get('_flash.old', []), $keys));
}
快閃記憶體資料的實現很簡單,session
中會維護兩個陣列:_flash.new
、_flash.old
,每次 session
結束前,都會刪除 _flash.old
中的儲存的 key
對應儲存在 session
的 value
。
now 方法
now
方法用於儲存只有本次請求採用的資料
public function now($key, $value)
{
$this->put($key, $value);
$this->push('_flash.old', $key);
}
reflash 方法
如果需要保留快閃記憶體資料給更多請求,可以使用 reflash 方法,這將會將所有的快閃記憶體資料保留給其他請求。
public function reflash()
{
$this->mergeNewFlashes($this->get('_flash.old', []));
$this->put('_flash.old', []);
}
這樣,_flash.old
中的資料就會被合併到 _flash.new
中。
keep 方法
只想保留特定的快閃記憶體資料給更多請求,則可以使用 keep 方法:
public function keep($keys = null)
{
$this->mergeNewFlashes($keys = is_array($keys) ? $keys : func_get_args());
$this->removeFromOldFlashData($keys);
}
forget 方法
forget 方法可以從 Session 內刪除一條資料。
public function forget($keys)
{
Arr::forget($this->attributes, $keys);
}
flush 方法
如果你想刪除 Session 內所有資料,可以使用 flush 方法:
public function flush()
{
$this->attributes = [];
}
重新生成 Session ID
重新生成 Session ID
,通常是為了防止惡意使用者利用 session fixation
對應用進行攻擊。如果使用了內建函式 LoginController
,Laravel 會自動重新生成身份驗證中 Session ID
。否則,你需要手動使用 regenerate
方法重新生成 Session ID
。
public function regenerate($destroy = false)
{
return $this->migrate($destroy);
}
public function migrate($destroy = false)
{
if ($destroy) {
$this->handler->destroy($this->getId());
}
$this->setExists(false);
$this->setId($this->generateSessionId());
return true;
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結