最近由於專案開發需要,手機客戶端和網頁端統一使用一套接口,為保證 會話(Session) 能夠正常且在各類情況下兼容,我希望能夠改變 SessionID 的獲取方式。預設情況下,所有網站都是透過 HTTP 請求的 Header 頭部中的 Cookie 實現的,透過 Cookie 中指定的 SessionID 來關聯到服務端對應數據,從而實現會話功能。
但對於手機用戶端,可能並不會支援原始的 Cookie,也或根據平台需求而屏蔽,因此開發中要求透過增加一個請求頭 X-Session-Token 來識別 SessionID。在 Laravel 框架中,實作 Session 初始化、讀取和啟動,都是透過 IlluminateSessionMiddlewareStartSession 這個中間件實現的,這個中間件有一個關鍵方法 getSession ,這個方法就是取得 SessionId 從而告知 Session 元件以什麼憑證恢復 Session 資料。
此中介軟體註冊於 app/Http/Kernel.php 檔案下。
我新建了一個類別繼承該中間件,同時替換了在app/Http/Kernel.php 下的註冊的地方,原來的getSession 方法源碼如下:
public function getSession(Request $request) { $session = $this->manager->driver(); $session->setId($request->cookies->get($session->getName())); return $session; }
在新的中間件中,我修改為:
public function getSession(Request $request) { $session = $this->manager->driver(); // 判断是否是接口访问并根据实际情况选择 SessionID 的获取方式 if ($request->headers->has('x-session-token')) { $sessionId = $request->headers->has('x-session-token'); } else { $sessionId = $request->cookies->get($session->getName()); } $session->setId($sessionId); return $session; }
但是麻煩也隨之而來。 。 。
修改完後,推送至分支,在合併至主開發分支之前往往需要跑一下單元測試,不幸的是,之前通過的Case 這回竟然報錯,問題是CSRF 組件報出Token 錯誤,而我們在這一處提供的Token 跟平時並無二致,問題肯定出在Session 上。
值得注意的是,我修改中間件的程式碼,對框架的影響可以說根本沒有,事實上也確實沒有,因為我將我自己創建的中間件代碼修改成繼承的中間件代碼一致也無濟於事,但奇怪的是,在我將中間件換回原來的中間件就沒有這個問題。
於是我將正常情況下和非正常情況下的程式碼都跑了一遍,在關鍵處斷點調試,發現問題出在中間件的一個重要屬性$sessionHandled , 若該值為false 則會引起我們之前的狀況。關鍵在於,中間件啟動之時,都會走 handle 方法,而對於 Session 這個中間件, handle 方法的第一行程式碼就是:
Interesting。 。 。 我們知道。 Laravel 框架的特色是其 IoC 容器,框架中初始化各種類別都是由其負責以實現各種依賴注入,以確保組件間的松耦合。中間件定然不例外。要知道,單例和普通實例最大的區別在於無論創建多少次,單例永遠都是一個,實例中的屬性不會被初始化,因此無問題的中間件必然是一個單例,而我自己創建的中間件只是個普通的類別的實例。但本著知其然更要知其所以然,我需要確認我這一想法(其實解決方法已經想到了,後面說)。 那麼問題大致就在於初始化中間件這塊了,於是不得不打起精神,仔細理一下 Laravel 的啟動程式碼。而這裡面的重點,在於一個叫做 IlluminatePipelinePipeline 的類別。這個類別有三個重要方法 send 、 through 、 then 。其中 then 是開始一切的鑰匙。這個類別主要是連續執行幾個框架啟動步驟的玩意兒,首先是初始化處理過程所需的元件(Request 和中間件),其次是將請求透過這些處理元件構成的堆疊(一堆中間件和路由派發元件),最後是返回處理結果(Response)。
protected function getSlice() { return function ($stack, $pipe) { return function ($passable) use ($stack, $pipe) { if ($pipe instanceof Closure) { return call_user_func($pipe, $passable, $stack); } else { list($name, $parameters) = $this->parsePipeString($pipe); return call_user_func_array([$this->container->make($name), $this->method], array_merge([$passable, $stack], $parameters)); } }; }; }可以注意到$this->container->make($name) ,這意味著其初始化一個中間件類,單純的就是make,若其不是單例則反覆new ,導致之前的屬性被初始化。
那麼解決辦法也顯而易見,使其成為一個單例。