Laravel Session——session 的啟動與執行原始碼分析

leoyang發表於2017-11-19

前言

本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/lar...

在網頁開發中, session 具有重要的作用,它可以在多個請求中儲存使用者的資訊,用於識別使用者的身份資訊。laravel 為使用者提供了可讀性強的 API 處理各種自帶的 Session 後臺驅動程式。支援諸如比較熱門的 Memcached、Redis 和開箱即用的資料庫等常見的後臺驅動程式。本文將會在本篇文章中講述最常見的由 Fileredis 驅動的 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.storesession 服務的儲存驅動。

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 作為 sessionPreviousUrl
  • 將當前的 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 中,sessionManagersession 服務的門面類,負責 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 對應儲存在 sessionvalue

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 協議》,轉載必須註明作者和本文連結

相關文章