HTTP Session
簡介
由於使用 HTTP 的應用程式是無狀態的 (Stateless),因此 Session 提供了能在多個 Request 間儲存有關使用者資訊的方法。這個使用者資訊通常儲存於持續性存放空間 (Persistent Store) 或後端中,能讓我們在之後的 Request 中存取。
Laravel 隨附了多種 Session 後端,能讓我們使用直觀且同一的 API 來存取 Session。支援的後端包含常見的 Memcached、Redis、與資料庫。
設定
專案的 Session 設定檔存在 config/session.php
中。建議先閱讀該檔案了解一下有哪些可用的選項。預設情況下,Laravel 設定使用 file
Session Driver,對於大多數的專案來說,都可以使用這個 Driver。若你的網站會在多個 Web Server (網頁伺服器) 間做 Load Balance (負載平衡),那我們就會需要選擇一種集中式的存放方案,如 Redis 或資料庫。
Session 的 driver
設定定義了每個 Request 的 Session 資料要存在哪裡。Laravel 隨附了多個不錯的 Driver:
-
file
- Session 儲存在storage/framework/sessions
。 -
cookie
- Session 儲存在安全的加密 Cookie 中。 -
database
- Session 儲存在關聯式資料庫中。 -
memcached
/redis
- Session 儲存在其中一個快速、基於快取的存放空間中。 -
dynamodb
- Session 儲存在 AWS DynamoDB。 -
array
- Session 儲存在 PHP 陣列中,且不會被持續保存。
Array Driver 主要是用在測試上的,會讓保存在 Session 裡的資料不被持續保存。
Driver 前置需求
Database
使用 database
Session Driver 時,需要先建立用來保存 Session 紀錄的資料表。下列是一個 Session 紀錄資料表的 Schema
定義範例:
1use Illuminate\Database\Schema\Blueprint;2use Illuminate\Support\Facades\Schema;34Schema::create('sessions', function (Blueprint $table) {5 $table->string('id')->primary();6 $table->foreignId('user_id')->nullable()->index();7 $table->string('ip_address', 45)->nullable();8 $table->text('user_agent')->nullable();9 $table->text('payload');10 $table->integer('last_activity')->index();11});
1use Illuminate\Database\Schema\Blueprint;2use Illuminate\Support\Facades\Schema;34Schema::create('sessions', function (Blueprint $table) {5 $table->string('id')->primary();6 $table->foreignId('user_id')->nullable()->index();7 $table->string('ip_address', 45)->nullable();8 $table->text('user_agent')->nullable();9 $table->text('payload');10 $table->integer('last_activity')->index();11});
可以使用 session:table
Artisan 指令來產生這個 Migration。若要瞭解更多資料庫 Migration 的資訊,請參考完整的 Migration 說明文件:
1php artisan session:table23php artisan migrate
1php artisan session:table23php artisan migrate
Redis
在 Laravel 上使用 Redis Session 前,必須先使用 PECL 安裝 PhpRedis PHP 擴充程式,或是使用 Composer 安裝 predis/predis
套件 (~1.0)。更多有關設定 Redis 的資訊,請參考 Laravel 的 Redis 說明文件。
在 session
設定檔中,可使用 connection
選項來指定 Session 要使用哪個 Redis 連線。
使用 Session
取得資料
在 Laravel 中有兩種使用 Session 的方式:全域 session
輔助函式,或是 Request
實體。首先,我們先來看看如何使用 Request
實體來存取 Session。可以在 Route 閉包或 Controller 方法上型別提示 (Type-Hint) Request。請記住,Controller 方法的相依性會自動由 Laravel 的 Service Container 插入:
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use Illuminate\Http\Request;7use Illuminate\View\View;89class UserController extends Controller10{11 /**12 * Show the profile for the given user.13 */14 public function show(Request $request, string $id): View15 {16 $value = $request->session()->get('key');1718 // ...1920 $user = $this->users->find($id);2122 return view('user.profile', ['user' => $user]);23 }24}
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use Illuminate\Http\Request;7use Illuminate\View\View;89class UserController extends Controller10{11 /**12 * Show the profile for the given user.13 */14 public function show(Request $request, string $id): View15 {16 $value = $request->session()->get('key');1718 // ...1920 $user = $this->users->find($id);2122 return view('user.profile', ['user' => $user]);23 }24}
從 Session 中取得資料時,也可以傳入一個預設值作為第二個引數給 get
方法。當 Session 中沒有指定的索引鍵時,就會回傳該索引值。若將閉包傳入作為預設值給 get
,且要求的索引鍵不存在時,就會執行該閉包並回傳執行的結果:
1$value = $request->session()->get('key', 'default');23$value = $request->session()->get('key', function () {4 return 'default';5});
1$value = $request->session()->get('key', 'default');23$value = $request->session()->get('key', function () {4 return 'default';5});
全域 Session 輔助函式
也可以使用全域的 session
PHP 函式來從 Session 中取得或儲存資料。呼叫 session
輔助函式時若只提供一個字串參數,則會回傳該 Session 索引鍵的值。呼叫 session
輔助函式時若提供一組索引鍵 / 值配對的陣列,則會將該陣列的值儲存在 Session 中:
1Route::get('/home', function () {2 // 從 Session 中取得資料...3 $value = session('key');45 // 指定預設值...6 $value = session('key', 'default');78 // 在 Session 中保存資料...9 session(['key' => 'value']);10});
1Route::get('/home', function () {2 // 從 Session 中取得資料...3 $value = session('key');45 // 指定預設值...6 $value = session('key', 'default');78 // 在 Session 中保存資料...9 session(['key' => 'value']);10});
使用 HTTP Request 實體跟全域 session
輔助函式在實務上沒有太大的不同。不管是哪種方式都是可測試的,測試時可以使用測試例中的 assertSessionHas
方法來測試。
取得所有 Session 資料
若想從 Session 中取得所有資料,可以使用 all
方法:
1$data = $request->session()->all();
1$data = $request->session()->all();
判斷 Session 中某個項目是否存在
若要判斷 Session 中是否有某個項目,可使用 has
方法。has
方法會在該項目存在且不為 null
時回傳 true
:
1if ($request->session()->has('users')) {2 // ...3}
1if ($request->session()->has('users')) {2 // ...3}
若想判斷某個項目是否存在 Session,且不論其值是否為 null
,可使用 exists
方法:
1if ($request->session()->exists('users')) {2 // ...3}
1if ($request->session()->exists('users')) {2 // ...3}
若要判斷 Session 中是否沒有某個項目,可使用 missing
方法。missing
方法會在該項目不存在時回傳 true
:
1if ($request->session()->missing('users')) {2 // ...3}
1if ($request->session()->missing('users')) {2 // ...3}
保存資料
若要將資料保存到 Session,我們通常會使用 Request 實體的 put
方法或全域的 session
輔助函式:
1// 使用 Request 實體...2$request->session()->put('key', 'value');34// 使用全域的「session」輔助函式...5session(['key' => 'value']);
1// 使用 Request 實體...2$request->session()->put('key', 'value');34// 使用全域的「session」輔助函式...5session(['key' => 'value']);
在 Session 值中推入陣列資料
可以使用 push
方法來將值推入 (Push) 到陣列的 Session 值中。舉例來說,若 user.teams
索引鍵中包含了一組團隊名稱陣列,我們可以像這樣將一個新的值推入陣列中:
1$request->session()->push('user.teams', 'developers');
1$request->session()->push('user.teams', 'developers');
取得與刪除項目
使用 pull
方法即可以單一陳述式從 Session 內取得並刪除某個項目:
1$value = $request->session()->pull('key', 'default');
1$value = $request->session()->pull('key', 'default');
遞增或遞減 Session 值
若 Session 資料中包含要遞增或遞減的整數,可以使用 increment
(遞增) 與 decrement
(遞減) 方法:
1$request->session()->increment('count');23$request->session()->increment('count', $incrementBy = 2);45$request->session()->decrement('count');67$request->session()->decrement('count', $decrementBy = 2);
1$request->session()->increment('count');23$request->session()->increment('count', $incrementBy = 2);45$request->session()->decrement('count');67$request->session()->decrement('count', $decrementBy = 2);
快閃資料
有時候,我們可能會想保存一些資料在 Session 中以供下一個 Request 使用。為此,我們可以使用 flash
方法。使用這個方法儲存在 Session 中的資料會在緊接著這個 Request 的下一個 HTTP Request 中可用。在下一個 HTTP Request 執行完成後,快閃資料就會被刪掉。快閃資料特別適合用於生命週期短的 (Short-Lived) 狀態訊息:
1$request->session()->flash('status', 'Task was successful!');
1$request->session()->flash('status', 'Task was successful!');
若想將快閃資料維持在好幾個 Request 中,可使用 reflash
方法。該方法會將所有的快閃資料都再維持一個 Request。若有需要保存特定的快閃資料,可使用 keep
方法:
1$request->session()->reflash();23$request->session()->keep(['username', 'email']);
1$request->session()->reflash();23$request->session()->keep(['username', 'email']);
若只想在目前 Request 中維持快閃資料,可使用 now
方法:
1$request->session()->now('status', 'Task was successful!');
1$request->session()->now('status', 'Task was successful!');
刪除資料
使用 forget
方法可從 Session 中刪除一筆資料。若想移除 Session 中的所有資料,可使用 flush
方法:
1// 刪除單一索引鍵...2$request->session()->forget('name');34// 刪除多個索引鍵...5$request->session()->forget(['name', 'status']);67$request->session()->flush();
1// 刪除單一索引鍵...2$request->session()->forget('name');34// 刪除多個索引鍵...5$request->session()->forget(['name', 'status']);67$request->session()->flush();
重新產生 Session ID
一般來說,重新產生 Session ID 是為了防止惡意使用者利用 Session Fixation 弱點攻擊你的程式。
如果你使用其中一種 Laravel 的專案入門套件,或是 Laravel Fortify,則 Laravel 會在登入時自動重新產生 Session ID。不過,若有需要手動重新產生 Session ID,可使用 regenerate
方法:
1$request->session()->regenerate();
1$request->session()->regenerate();
若有需要以單一陳述式重新產生 Session ID 並從 Session 中移除所有資料的話,可使用 invalidate
方法:
1$request->session()->invalidate();
1$request->session()->invalidate();
Session 封鎖
若要使用 Session 鎖定,必須要使用支援 Atomic Lock (不可部分完成鎖定) 的快取 Driver。目前,支援 Atomic Lock 的快取 Driver 有 memcached
、dynamodb
、redis
、database
等 Driver。此外,也沒辦法使用 cookie
Session Driver。
預設情況下,Laravel 能讓多個 Request 使用相同的 Session 來同步執行。不過,舉例來說,若我們使用某個 JavaScript HTTP 函式庫來建立兩個連到我們專案的 HTTP Request,且這兩個 Request 會同時執行。對於大多數的專案來說,這不會有什麼問題。不過,對一部分的專案,如果這兩個 Request 送往兩個不同的 Endpoint (端點),且這兩個 Endpoint 都有寫入資料到 Session 的話,就有可能會發生 Session 資料遺失的問題。
為了解決這個問題,Laravel 提供了能讓我們針對給定 Session 限制同步 Request 數量的功能。要開始使用 Session 封鎖,我們只需要在 Route 定義後方串上 block
方法即可。在這個例子中,所有連入到 /profile
Endpoint 的 Request 都會取得一個 Session Lock (鎖定)。當被 Lock 時,所有連到 /profile
或 /order
Endpoint 的 Request 若有相同的 Session ID,都必須等到第一個 Request 執行完成後,才能繼續執行:
1Route::post('/profile', function () {2 // ...3})->block($lockSeconds = 10, $waitSeconds = 10)45Route::post('/order', function () {6 // ...7})->block($lockSeconds = 10, $waitSeconds = 10)
1Route::post('/profile', function () {2 // ...3})->block($lockSeconds = 10, $waitSeconds = 10)45Route::post('/order', function () {6 // ...7})->block($lockSeconds = 10, $waitSeconds = 10)
block
方法接受兩個可選的引數。block
方法的第一個引數 Session 要被 Lock 的最大秒數。當然,若 Request 比這個時間還要早完成執行的話,也會提早釋放 Lock:
block
方法的第二個引數是 Request 在取得 Session Lock 前應等待的秒數。若在給定的秒數後 Request 仍然無法取得 Session Lock 的話,會擲回 Illuminate\Contracts\Cache\LockTimeoutException
。
若沒有提供這些引數,則 Lock 最長可取得 10 秒,而 Request 在取得 Lock 時最多可等待 10 秒:
1Route::post('/profile', function () {2 // ...3})->block()
1Route::post('/profile', function () {2 // ...3})->block()
新增自訂 Session Driver
實作 Driver
若現有的 Session Driver 都無法滿足你的專案需求,在 Laravel 中也可以撰寫你自己的 Session 處理常式 (Handler)。自訂 Session Driver 應實作 PHP 內建的 SessionHandlerInterface
。這個介面只包含了幾個簡單的方法。MongoDB 實作的 Stub (虛設常式) 看起來會像這樣:
1<?php23namespace App\Extensions;45class MongoSessionHandler implements \SessionHandlerInterface6{7 public function open($savePath, $sessionName) {}8 public function close() {}9 public function read($sessionId) {}10 public function write($sessionId, $data) {}11 public function destroy($sessionId) {}12 public function gc($lifetime) {}13}
1<?php23namespace App\Extensions;45class MongoSessionHandler implements \SessionHandlerInterface6{7 public function open($savePath, $sessionName) {}8 public function close() {}9 public function read($sessionId) {}10 public function write($sessionId, $data) {}11 public function destroy($sessionId) {}12 public function gc($lifetime) {}13}
Laravel 中沒有內建用來放置擴充程式的目錄。你可以自由放置這些擴充程式。在這個例子中,我們建立了一個 Extensions
目錄來放置 MongoSessionHandler
。
由於只看這些方法很難看出他們的功能,所以我們來快速看一下各個方法都用來做什麼:
-
open
方法通常是給一些基於檔案的 Session 存放系統使用的。因為 Laravel 已經有附帶file
Session Driver 了,所以通常這個方法裡應該不需要寫什麼內容。留空即可。 -
close
方法跟open
方法一樣,通常可以忽略。對大多數的 Driver 來說並不需要。 -
read
方法應回傳與給定$sessionId
關聯的字串版本 Session 資料。在從 Driver 中取出資料時不需要進行任何的序列化 或其他編碼,因為 Laravel 會幫你序列化。 -
write
方法應將給定的$data
字串以$sessionId
關聯並保存到儲存系統中,例如 MongoDB 或其他你選擇的儲存系統。同樣的,不需要進行任何序列化 —— Laravel 已經幫你序列化好了。 -
destroy
方法從持續性儲存系統中移除任何與$sessionId
關聯的資料。 -
gc
方法移除所有時間舊於$lifetime
的Session
資料。$lifetime
是 UNIX 時戳。對於自帶有效期限的系統,如 Memcached 或 Redis,可以將這個方法留空。
註冊 Driver
實作好 Driver 後,就可以將該 Driver 註冊到 Laravel。若要將額外的 Driver 新增到 Laravel 的 Session 後端中,我們可以使用 Session
Facade 的 extend
方法。可以在某個 Service Provider 中呼叫這個 extend
方法。可以使用現有的 App\Providers\AppServiceProvider
,或是建立一個全新的 Provider:
1<?php23namespace App\Providers;45use App\Extensions\MongoSessionHandler;6use Illuminate\Contracts\Foundation\Application;7use Illuminate\Support\Facades\Session;8use Illuminate\Support\ServiceProvider;910class SessionServiceProvider extends ServiceProvider11{12 /**13 * Register any application services.14 */15 public function register(): void16 {17 // ...18 }1920 /**21 * Bootstrap any application services.22 */23 public function boot(): void24 {25 Session::extend('mongo', function (Application $app) {26 // 回傳 SessionHandlerInterface 的實作...27 return new MongoSessionHandler;28 });29 }30}
1<?php23namespace App\Providers;45use App\Extensions\MongoSessionHandler;6use Illuminate\Contracts\Foundation\Application;7use Illuminate\Support\Facades\Session;8use Illuminate\Support\ServiceProvider;910class SessionServiceProvider extends ServiceProvider11{12 /**13 * Register any application services.14 */15 public function register(): void16 {17 // ...18 }1920 /**21 * Bootstrap any application services.22 */23 public function boot(): void24 {25 Session::extend('mongo', function (Application $app) {26 // 回傳 SessionHandlerInterface 的實作...27 return new MongoSessionHandler;28 });29 }30}
註冊好 Session Driver 後,就可以在 config/session.php
設定檔中使用 mongo
Driver。