Day 20 多語系用到了 session(),今天把 Session 跟 Cookie 這兩個 HTTP 世界裡「記住狀態」的基礎機制講清楚。這塊在 Laravel 10 到 13 之間沒有版本差異,適合當作前面幾天新骨架、新中介層知識的一次實作練習。
// 設定 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
某些第三方服務(例如金流商)可能需要讀取未加密的原始 Cookie 值,這種情況要在 bootstrap/app.php 排除特定 Cookie 不經過加密(延續 Day 16 學到的中介層設定語法):
->withMiddleware(function (Middleware $middleware) {
$middleware->encryptCookies(except: [
'third_party_tracking_id',
]);
})
Day 16 提過 Laravel 會自動在每個回應附加一個加密的 XSRF-TOKEN Cookie,這個 Cookie 跟今天講的一般 Cookie 是同一套機制,只是用途特殊——前端 JavaScript 框架(例如 SPA 情境)讀取這個 Cookie 的值,放進後續 AJAX 請求的 X-XSRF-TOKEN 標頭,達成不需要每個表單都手動塞 @csrf 隱藏欄位的效果。這也是為什麼 Day 16 排除 CSRF 驗證的路由(例如 Webhook),通常也不需要擔心 Cookie 加密機制的干擾——Webhook 請求本來就不是瀏覽器發出的,不會帶有這些 Cookie。
Session 是伺服器端儲存「跨多次請求」狀態的機制(Cookie 通常只存一個 Session ID,實際資料存在伺服器端)。

config/session.php 決定資料實際存放位置:
| Driver | 說明 |
|---|---|
file |
存成檔案,storage/framework/sessions |
cookie |
直接加密存進 Cookie(不佔伺服器儲存空間,但有大小限制) |
database |
存進資料庫,Laravel 11+ 新專案的常見選擇之一 |
redis/memcached |
存進快取伺服器,效能好,適合多台伺服器共用 Session 的情境 |
array |
只存在記憶體,請求結束就消失,測試環境常用 |
多台伺服器(負載平衡)部署時,不能用 file 驅動,因為使用者的請求可能被導到不同台伺服器,各自的本地檔案系統看不到彼此的 Session 資料,這種情境要用 database、redis 這類集中儲存的驅動。
config/session.php 還有幾個容易被忽略、但影響使用者體驗的設定:
'lifetime' => env('SESSION_LIFETIME', 120), // Session 閒置多少分鐘後過期
'expire_on_close' => false, // 是否在瀏覽器關閉時就讓 Session 失效
'lottery' => [2, 100], // 每次請求有 2% 機率觸發過期 Session 的垃圾回收清理
lottery 這個設定值得認識——它決定「清除過期 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
使用者登入成功後,Laravel 的認證系統(Fortify,Day 4)會自動重新產生一組新的 Session ID,你也可以在自己的邏輯裡手動觸發:
$request->session()->regenerate();
這個機制是為了防範「Session Fixation」攻擊——如果攻擊者事先誘導受害者使用一組攻擊者已知的 Session ID,受害者登入後如果沒有換發新 ID,攻擊者就能用原本已知的 ID 冒充已登入的受害者。這是資安領域的標準防護手段,Laravel 內建的認證流程已經自動處理,如果你自己寫了不透過 Fortify 的自訂登入邏輯,記得手動呼叫 regenerate()。
表單送出後常見的「操作成功」提示,是 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 資料「用一次就消失」的行為有時候不完全符合需求——例如一個多步驟表單,使用者在步驟 2 按了「上一步」回到步驟 1,你可能希望步驟 1 顯示的提示訊息能再多存活一次請求:
$request->session()->reflash(); // 把「所有」flash 資料的存活期限再延長一次請求
$request->session()->keep(['success']); // 只延長「指定」的 flash 資料
keep() 比 reflash() 更精確,適合你只想延長某幾筆 flash 資料、其餘的維持正常「用一次就消失」行為的情境。
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 的值就是 null,has() 會回傳 false(因為它同時檢查「存在」且「不是 null」),exists() 只檢查「這個 key 存在過」,兩者語意不同,用錯容易導致邏輯誤判。file/database 驅動雖然存在伺服器端相對安全,但仍建議只存必要的最小資料。regenerate():如果沒有透過 Fortify 內建的認證流程、自己手動處理登入,忘記重新產生 Session ID 會留下 Session Fixation 的資安風險。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。