為支援手機客戶端 X-Session-Token Header 傳遞 Session ID,開發者繼承 Illuminate\Session\Middleware\StartSession 並覆寫 getSession() 方法,替換 app/Http/Kernel.php 中的註冊。單元測試卻報 **CSRF Token 驗證失敗**,Session 實際未寫入。排查發現:Laravel Pipeline 每次請求都 `new` 中間件類,非單例導致 `sessionHandled` 狀態丟失。解決方案:在 Service Provider 中以 **單例綁定** 自定義中間件。
<?php
// 自定義中間件 app/Http/Middleware/CustomStartSession.php
namespace App\Http\Middleware;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Http\Request;
class CustomStartSession extends StartSession {
public function getSession(Request $request) {
$session = $this->manager->driver();
// 支援 X-Session-Token Header
if ($request->headers->has('x-session-token')) {
$sessionId = $request->headers->get('x-session-token');
} else {
$sessionId = $request->cookies->get($session->getName());
}
$session->setId($sessionId);
return $session;
}
}
// Kernel.php 註冊替換
protected $middleware = [
// \Illuminate\Session\Middleware\StartSession::class, // 原
\App\Http\Middleware\CustomStartSession::class, // 新
];
現象:原本通過的單元測試(CSRF 驗證)全部失敗,報 TokenMismatchException。換回原中間件即恢復正常。
StartSession 中關鍵屬性 protected $sessionHandled = false;。handle() 方法首行:$this->sessionHandled = true;。$this->sessionHandled 為 true 時執行 Session 保存/寫入 Cookie。getSlice():// Illuminate\Pipeline\Pipeline.php
protected function getSlice() {
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
if ($pipe instanceof Closure) { ... }
// 關鍵行:每次請求都通過容器 make 實例化!
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) **預設每次調用返回新實例**(非單例)。導致:
在 App\Providers\AppServiceProvider 的 register() 方法中註冊為單例:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Http\Middleware\CustomStartSession;
class AppServiceProvider extends ServiceProvider {
public function register() {
// 關鍵:綁定為單例,容器全局共享同一實例
$this->app->singleton(CustomStartSession::class);
}
public function boot() { }
}
?>
或在 Kernel.php 中直接使用字符串別名(需先在容器綁定別名):
$this->app->singleton('custom.session', CustomStartSession::class);
// Kernel.php
protected $middleware = [
'custom.session', // 解析時會從容器獲取單例
];
X-Session-Token 請求,Session 正常讀寫。| 場景 | 是否需單例 | 原因 |
|---|---|---|
| 有狀態屬性(如 $sessionHandled、計數器、緩存) | ✅ 必須 | 多實例導致狀態不共享 |
| 無狀態、純邏輯處理 | ❌ 可選 | 每次新實例無副作用 |
| 需在 handle() 前/後共享數據 | ✅ 必須 | 跨生命週期共享 |