首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

Laravel 擴展 Session 中間件導致寫入失效:Pipeline 實例化機制與單例註冊解決方案

     編輯:PHP 相關知識

Laravel 中間件擴展陷阱:繼承 StartSession 後 Session 寫入失效,根因在 Pipeline 非單例實例化

為支援手機客戶端 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。換回原中間件即恢復正常。

根因深度追蹤

  1. StartSession 中關鍵屬性 protected $sessionHandled = false;。
  2. handle() 方法首行:$this->sessionHandled = true;。
  3. 後續邏輯僅在 $this->sessionHandled 為 true 時執行 Session 保存/寫入 Cookie。
  4. Laravel Pipeline 處理中間件堆棧,核心方法 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) **預設每次調用返回新實例**(非單例)。導致:

  • 請求開始 → Pipeline `new CustomStartSession()` → `handle()` 設 `$sessionHandled=true` → 執行邏輯。
  • 請求結束 → 再次 `new CustomStartSession()`(或其他地方解析)→ **新實例 `$sessionHandled=false`** → 跳過保存邏輯 → **Session 不寫入**。

解決方案:單例綁定自定義中間件

在 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', // 解析時會從容器獲取單例
];

驗證修復

  • 單元測試全部通過(CSRF Token 正常驗證)。
  • 手機客戶端攜帶 X-Session-Token 請求,Session 正常讀寫。
  • Web 端 Cookie 模式不受影響。

舉一反三:Laravel 中間件開發鐵律

最佳實踐:

  1. 所有**自定義中間件預設在 ServiceProvider 中 `singleton()` 綁定**。
  2. 若確確實實無狀態,可不綁定,但綁定無副作用(容器單例模式極輕量)。
  3. 使用 $this->app->singleton(Middleware::class) 而非 bind 或直接類名。

總結

Laravel Pipeline 預設**每請求實例化中間件**,這對無狀態中間件無影響,但對 StartSession 這類**依賴實例屬性跨請求階段協作**的中間件是致命的。**將自定義中間件註冊為容器單例**,是繼承框架核心中間件時的必修動作。此問題隱蔽性極強(代碼邏輯完全正確),僅通過單元測試或生產異常暴露,值得所有 Laravel 開發者納入「中間件開發檢查清單」。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
場景是否需單例原因
有狀態屬性(如 $sessionHandled、計數器、緩存)✅ 必須多實例導致狀態不共享
無狀態、純邏輯處理❌ 可選每次新實例無副作用
需在 handle() 前/後共享數據✅ 必須跨生命週期共享