iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0
Modern Web

我推的Laravel S2!系列 第 25

我推的Laravel S2|Day 24:Session 與 Cookie

  • 分享至 

  • xImage
  •  

一句話破題

Day 20 多語系用到了 session(),今天把 Session 跟 Cookie 這兩個 HTTP 世界裡「記住狀態」的基礎機制講清楚。這塊在 Laravel 10 到 13 之間沒有版本差異,適合當作前面幾天新骨架、新中介層知識的一次實作練習。

Cookie

// 設定 Cookie 並附加到回應
return redirect()->route('posts.index')->withCookie(cookie('recently_viewed', $post->id, 60));

// 或用 Cookie facade 加進佇列,會在回應送出時自動附加
Cookie::queue('recently_viewed', $post->id, 60);

// 讀取
$recentlyViewed = $request->cookie('recently_viewed');

// 建立一個「永久」的 Cookie(實際上是設定一個很長的過期時間,例如 5 年)
Cookie::queue(Cookie::forever('preferred_theme', 'dark'));

// 立即清除一個 Cookie
Cookie::queue(Cookie::forget('recently_viewed'));

cookie()/Cookie::queue() 的第三個參數是分鐘數(多久後過期)。Laravel 建立的所有 Cookie 預設都經過加密並附帶簽章,代表如果使用者端手動竄改了 Cookie 值,Laravel 會直接視為無效,不會被程式碼誤用——這是內建的安全機制,不需要自己額外處理防竄改邏輯。

Blade 裡讀取:

@if ($post->id == Request::cookie('recently_viewed'))
    <span>你剛看過這篇</span>
@endif

排除加密:EncryptCookies 中介層

某些第三方服務(例如金流商)可能需要讀取未加密的原始 Cookie 值,這種情況要在 bootstrap/app.php 排除特定 Cookie 不經過加密(延續 Day 16 學到的中介層設定語法):

->withMiddleware(function (Middleware $middleware) {
    $middleware->encryptCookies(except: [
        'third_party_tracking_id',
    ]);
})

XSRF-TOKEN:跟 Day 16 CSRF 防護的關聯

Day 16 提過 Laravel 會自動在每個回應附加一個加密的 XSRF-TOKEN Cookie,這個 Cookie 跟今天講的一般 Cookie 是同一套機制,只是用途特殊——前端 JavaScript 框架(例如 SPA 情境)讀取這個 Cookie 的值,放進後續 AJAX 請求的 X-XSRF-TOKEN 標頭,達成不需要每個表單都手動塞 @csrf 隱藏欄位的效果。這也是為什麼 Day 16 排除 CSRF 驗證的路由(例如 Webhook),通常也不需要擔心 Cookie 加密機制的干擾——Webhook 請求本來就不是瀏覽器發出的,不會帶有這些 Cookie。

Session

Session 是伺服器端儲存「跨多次請求」狀態的機制(Cookie 通常只存一個 Session ID,實際資料存在伺服器端)。

Cookie 的實際資料留在瀏覽器端(已加密簽章);Session 只在瀏覽器留一個 ID,真正的資料存在伺服器端的 file/database/redis

config/session.php 決定資料實際存放位置:

Driver 說明
file 存成檔案,storage/framework/sessions
cookie 直接加密存進 Cookie(不佔伺服器儲存空間,但有大小限制)
database 存進資料庫,Laravel 11+ 新專案的常見選擇之一
redis/memcached 存進快取伺服器,效能好,適合多台伺服器共用 Session 的情境
array 只存在記憶體,請求結束就消失,測試環境常用

多台伺服器(負載平衡)部署時,不能用 file 驅動,因為使用者的請求可能被導到不同台伺服器,各自的本地檔案系統看不到彼此的 Session 資料,這種情境要用 databaseredis 這類集中儲存的驅動。

Session 的生命週期設定

config/session.php 還有幾個容易被忽略、但影響使用者體驗的設定:

'lifetime' => env('SESSION_LIFETIME', 120),   // Session 閒置多少分鐘後過期
'expire_on_close' => false,                    // 是否在瀏覽器關閉時就讓 Session 失效
'lottery' => [2, 100],                         // 每次請求有 2% 機率觸發過期 Session 的垃圾回收清理

lottery 這個設定值得認識——它決定「清除過期 Session 資料」這件維護工作,用多大的機率在每次請求時順便觸發一次,避免每個請求都要做一次昂貴的清理掃描,又能確保過期資料最終會被清乾淨,是一種常見的機率性維護策略(不需要額外設定排程任務)。

使用 Session

// 存值
$request->session()->put('cart_post_ids', [1, 2, 3]);
session(['cart_post_ids' => [1, 2, 3]]);   // 全域函式的簡寫寫法效果相同

// 讀值
$cartIds = session('cart_post_ids', []);   // 第二參數是預設值
$cartIds = $request->session()->get('cart_post_ids', []);

// 陣列操作
$request->session()->push('cart_post_ids', $newPostId);   // 附加進陣列
$value = $request->session()->pull('cart_post_ids');       // 取出並移除

// 判斷與清除
$request->session()->has('cart_post_ids');     // 存在且不是 null
$request->session()->exists('cart_post_ids');  // 只判斷是否存在過(就算值是 null 也算 true)
$request->session()->missing('cart_post_ids');
$request->session()->forget('cart_post_ids');  // 移除單一 key
$request->session()->flush();                   // 清空整個 session

// 只取部分/排除部分
$request->session()->only(['cart_post_ids']);
$request->session()->except(['cart_post_ids']);

$request->session()->all();   // 取得所有 session 資料

Blade 裡也能直接判斷 Session 是否存在:

@session('cart_post_ids')
    <span>購物車有 {{ count($value) }} 項</span>
@endsession

Session ID 重新產生:登入時的安全考量

使用者登入成功後,Laravel 的認證系統(Fortify,Day 4)會自動重新產生一組新的 Session ID,你也可以在自己的邏輯裡手動觸發:

$request->session()->regenerate();

這個機制是為了防範「Session Fixation」攻擊——如果攻擊者事先誘導受害者使用一組攻擊者已知的 Session ID,受害者登入後如果沒有換發新 ID,攻擊者就能用原本已知的 ID 冒充已登入的受害者。這是資安領域的標準防護手段,Laravel 內建的認證流程已經自動處理,如果你自己寫了不透過 Fortify 的自訂登入邏輯,記得手動呼叫 regenerate()

Flash 訊息:常見的「用一次就消失」模式

表單送出後常見的「操作成功」提示,是 Session 最典型的應用場景:

// PostController::store()
return redirect()->route('posts.index')->with('success', '文章已建立!');
@if (session('success'))
    <div class="alert alert-success">{{ session('success') }}</div>
@endif

->with() 存進的是「flash 資料」,只在下一次請求裡有效,讀取過後(或再下一次請求)就自動被清除,不需要自己手動 forget()

Flash 資料的進階控制:reflash 與 keep

Flash 資料「用一次就消失」的行為有時候不完全符合需求——例如一個多步驟表單,使用者在步驟 2 按了「上一步」回到步驟 1,你可能希望步驟 1 顯示的提示訊息能再多存活一次請求:

$request->session()->reflash();            // 把「所有」flash 資料的存活期限再延長一次請求
$request->session()->keep(['success']);    // 只延長「指定」的 flash 資料

keep()reflash() 更精確,適合你只想延長某幾筆 flash 資料、其餘的維持正常「用一次就消失」行為的情境。

測試 Session 相關邏輯

Day 27 會深入完整測試主題,這裡先提一個跟今天內容直接相關的技巧:

test('文章建立成功後顯示 flash 訊息', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->post('/posts', ['title' => '測試文章', 'body' => '內容內容內容內容'])
        ->assertSessionHas('success', '文章已建立!');
});

test('可以預先帶入 Session 資料測試相依邏輯', function () {
    $this->withSession(['locale' => 'en'])
        ->get('/posts')
        ->assertSee('Latest Posts');
});

withSession() 讓你在測試發出請求前,先模擬設定好一組 Session 資料,這在測試 Day 20 那種「依賴 Session 裡的語系設定」的邏輯時特別實用,不需要真的先發一次「切換語系」的請求才能測試後續行為。

常見錯誤與踩雷點

  • 在多台伺服器環境用 file Session 驅動:使用者可能忽然被登出或購物車內容消失,因為請求被導到了另一台看不到原本 Session 檔案的伺服器,正式環境部署前務必確認驅動跟部署架構相容。
  • 搞混 has()exists():如果 Session 裡某個 key 的值就是 nullhas() 會回傳 false(因為它同時檢查「存在」且「不是 null」),exists() 只檢查「這個 key 存在過」,兩者語意不同,用錯容易導致邏輯誤判。
  • 想用 Session 存放大量或敏感資料:Session 適合存少量、跟使用者當前操作狀態相關的資料(購物車 ID 清單、表單分步填寫的暫存值),不該拿來存放大型資料集或未加密的敏感資訊;file/database 驅動雖然存在伺服器端相對安全,但仍建議只存必要的最小資料。
  • 自訂登入邏輯忘記呼叫 regenerate():如果沒有透過 Fortify 內建的認證流程、自己手動處理登入,忘記重新產生 Session ID 會留下 Session Fixation 的資安風險。
  • 多分頁/多請求同時操作時,Flash 訊息互相覆蓋:使用者如果同時開兩個分頁操作同一個應用程式,Flash 資料是綁在同一個 Session 上的,兩個分頁的操作可能互相干擾對方的 Flash 訊息,這是 Session 機制本身的限制,設計跨分頁互動的功能時需要意識到這一點。

小結

Cookie 跟 Session 的核心 API 在 Laravel 10 到 13 間保持穩定,今天的重點是釐清兩者的定位差異(Cookie 存在客戶端、Session 存在伺服器端只留 ID 在客戶端)、Session 驅動的選擇(尤其是多台伺服器部署時不能用 file),以及 Flash 訊息這個非常實用的一次性資料模式,也補上了 Session ID 重新產生的資安考量跟 reflash()/keep() 這兩個進階控制方法。

溫故而知新,可以為師矣 — 《論語・為政》

明日預告

Day 25 進入 Schedule 排程與自訂 Artisan Command,排程定義從已消失的 app/Console/Kernel.php 搬到 routes/console.php,改用 Schedule facade。


上一篇
我推的Laravel S2|Day 23:Service Container 與 Service Provider:依賴注入實戰
下一篇
我推的Laravel S2|Day 25:Schedule 排程與自訂 Artisan Command
系列文
我推的Laravel S2!27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言