HTTP Session

簡介

由於使用 HTTP 的應用程式是無狀態的 (Stateless),因此 Session 提供了能在多個 Request 間儲存有關使用者資訊的方法。這個使用者資訊通常儲存於持續性存放空間 (Persistent Store) 或後端中,能讓我們在之後的 Request 中存取。

Laravel 隨附了多種 Session 後端,能讓我們使用直觀且同一的 API 來存取 Session。支援的後端包含常見的 MemcachedRedis、與資料庫。

設定

專案的 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 陣列中,且不會被持續保存(Persist)
lightbulb

Array Driver 主要是用在測試上的,會讓保存在 Session 裡的資料不被持續保存。

Driver 前置需求

Database

使用 database Session Driver 時,需要先建立用來保存 Session 紀錄的資料表。下列是一個 Session 紀錄資料表的 Schema 定義範例:

1Schema::create('sessions', function ($table) {
2 $table->string('id')->primary();
3 $table->foreignId('user_id')->nullable()->index();
4 $table->string('ip_address', 45)->nullable();
5 $table->text('user_agent')->nullable();
6 $table->text('payload');
7 $table->integer('last_activity')->index();
8});
1Schema::create('sessions', function ($table) {
2 $table->string('id')->primary();
3 $table->foreignId('user_id')->nullable()->index();
4 $table->string('ip_address', 45)->nullable();
5 $table->text('user_agent')->nullable();
6 $table->text('payload');
7 $table->integer('last_activity')->index();
8});

可以使用 session:table Artisan 指令來產生這個 Migration。若要瞭解更多資料庫 Migration 的資訊,請參考完整的 Migration 說明文件

1php artisan session:table
2 
3php artisan migrate
1php artisan session:table
2 
3php artisan migrate

Redis

在 Laravel 上使用 Redis Session 前,必須先使用 PECL 安裝 PhpRedis PHP 擴充程式,或是使用 Composer 安裝 predis/predis 套件 (~1.0)。更多有關設定 Redis 的資訊,請參考 Laravel 的 Redis 說明文件

lightbulb

session 設定檔中,可使用 connection 選項來指定 Session 要使用哪個 Redis 連線。

使用 Session

取得資料

在 Laravel 中有兩種使用 Session 的方式:全域 session 輔助函式,或是 Request 實體。首先,我們先來看看如何使用 Request 實體來存取 Session。可以在 Route 閉包或 Controller 方法上型別提示 (Type-Hint) Request。請記住,Controller 方法的相依性會自動由 Laravel 的 Service Container 插入:

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Http\Controllers\Controller;
6use Illuminate\Http\Request;
7 
8class UserController extends Controller
9{
10 /**
11 * Show the profile for the given user.
12 *
13 * @param Request $request
14 * @param int $id
15 * @return Response
16 */
17 public function show(Request $request, $id)
18 {
19 $value = $request->session()->get('key');
20 
21 //
22 }
23}
1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Http\Controllers\Controller;
6use Illuminate\Http\Request;
7 
8class UserController extends Controller
9{
10 /**
11 * Show the profile for the given user.
12 *
13 * @param Request $request
14 * @param int $id
15 * @return Response
16 */
17 public function show(Request $request, $id)
18 {
19 $value = $request->session()->get('key');
20 
21 //
22 }
23}

從 Session 中取得資料時,也可以傳入一個預設值作為第二個引數給 get 方法。當 Session 中沒有指定的索引鍵時,就會回傳該索引值。若將閉包傳入作為預設值給 get,且要求的索引鍵不存在時,就會執行該閉包並回傳執行的結果:

1$value = $request->session()->get('key', 'default');
2 
3$value = $request->session()->get('key', function () {
4 return 'default';
5});
1$value = $request->session()->get('key', 'default');
2 
3$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');
4 
5 // 指定預設值...
6 $value = session('key', 'default');
7 
8 // 在 Session 中保存資料...
9 session(['key' => 'value']);
10});
1Route::get('/home', function () {
2 // 從 Session 中取得資料...
3 $value = session('key');
4 
5 // 指定預設值...
6 $value = session('key', 'default');
7 
8 // 在 Session 中保存資料...
9 session(['key' => 'value']);
10});
lightbulb

使用 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');
3 
4// 使用全域的「session」輔助函式...
5session(['key' => 'value']);
1// 使用 Request 實體...
2$request->session()->put('key', 'value');
3 
4// 使用全域的「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');
2 
3$request->session()->increment('count', $incrementBy = 2);
4 
5$request->session()->decrement('count');
6 
7$request->session()->decrement('count', $decrementBy = 2);
1$request->session()->increment('count');
2 
3$request->session()->increment('count', $incrementBy = 2);
4 
5$request->session()->decrement('count');
6 
7$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();
2 
3$request->session()->keep(['username', 'email']);
1$request->session()->reflash();
2 
3$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');
3 
4// 刪除多個索引鍵...
5$request->session()->forget(['name', 'status']);
6 
7$request->session()->flush();
1// 刪除單一索引鍵...
2$request->session()->forget('name');
3 
4// 刪除多個索引鍵...
5$request->session()->forget(['name', 'status']);
6 
7$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 封鎖

exclamation

若要使用 Session 鎖定,必須要使用支援 Atomic Lock (不可部分完成鎖定) 的快取 Driver。目前,支援 Atomic Lock 的快取 Driver 有 memcacheddynamodbredisdatabase 等 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)
4 
5Route::post('/order', function () {
6 //
7})->block($lockSeconds = 10, $waitSeconds = 10)
1Route::post('/profile', function () {
2 //
3})->block($lockSeconds = 10, $waitSeconds = 10)
4 
5Route::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<?php
2 
3namespace App\Extensions;
4 
5class MongoSessionHandler implements \SessionHandlerInterface
6{
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<?php
2 
3namespace App\Extensions;
4 
5class MongoSessionHandler implements \SessionHandlerInterface
6{
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}
lightbulb

Laravel 中沒有內建用來放置擴充程式的目錄。你可以自由放置這些擴充程式。在這個例子中,我們建立了一個 Extensions 目錄來放置 MongoSessionHandler

由於只看這些方法很難看出他們的功能,所以我們來快速看一下各個方法都用來做什麼:

  • open 方法通常是給一些基於檔案的 Session 存放系統使用的。因為 Laravel 已經有附帶 file Session Driver 了,所以通常這個方法裡應該不需要寫什麼內容。留空即可。
  • close 方法跟 open 方法一樣,通常可以忽略。對大多數的 Driver 來說並不需要。
  • read 方法應回傳與給定 $sessionId 關聯的字串版本 Session 資料。在從 Driver 中取出資料時不需要進行任何的序列化(Serialization) 或其他編碼,因為 Laravel 會幫你序列化。
  • write 方法應將給定的 $data 字串以 $sessionId 關聯並保存到儲存系統中,例如 MongoDB 或其他你選擇的儲存系統。同樣的,不需要進行任何序列化 —— Laravel 已經幫你序列化好了。
  • destroy 方法從持續性儲存系統中移除任何與 $sessionId 關聯的資料。
  • gc 方法移除所有時間舊於 $lifetimeSession 資料。$lifetime 是 UNIX 時戳。對於自帶有效期限(Self-Expiring)的系統,如 Memcached 或 Redis,可以將這個方法留空。

註冊 Driver

實作好 Driver 後,就可以將該 Driver 註冊到 Laravel。若要將額外的 Driver 新增到 Laravel 的 Session 後端中,我們可以使用 Session Facadeextend 方法。可以在某個 Service Provider 中呼叫這個 extend 方法。可以使用現有的 App\Providers\AppServiceProvider,或是建立一個全新的 Provider:

1<?php
2 
3namespace App\Providers;
4 
5use App\Extensions\MongoSessionHandler;
6use Illuminate\Support\Facades\Session;
7use Illuminate\Support\ServiceProvider;
8 
9class SessionServiceProvider extends ServiceProvider
10{
11 /**
12 * Register any application services.
13 *
14 * @return void
15 */
16 public function register()
17 {
18 //
19 }
20 
21 /**
22 * Bootstrap any application services.
23 *
24 * @return void
25 */
26 public function boot()
27 {
28 Session::extend('mongo', function ($app) {
29 // Return an implementation of SessionHandlerInterface...
30 return new MongoSessionHandler;
31 });
32 }
33}
1<?php
2 
3namespace App\Providers;
4 
5use App\Extensions\MongoSessionHandler;
6use Illuminate\Support\Facades\Session;
7use Illuminate\Support\ServiceProvider;
8 
9class SessionServiceProvider extends ServiceProvider
10{
11 /**
12 * Register any application services.
13 *
14 * @return void
15 */
16 public function register()
17 {
18 //
19 }
20 
21 /**
22 * Bootstrap any application services.
23 *
24 * @return void
25 */
26 public function boot()
27 {
28 Session::extend('mongo', function ($app) {
29 // Return an implementation of SessionHandlerInterface...
30 return new MongoSessionHandler;
31 });
32 }
33}

註冊好 Session Driver 後,就可以在 config/session.php 設定檔中使用 mongo Driver。

翻譯進度
100% 已翻譯
更新時間:
2023年2月11日 上午10:28:00 [世界標準時間]
翻譯人員:
  • cornch
幫我們翻譯此頁

留言

尚無留言

“Laravel” is a Trademark of Taylor Otwell.
The source documentation is released under MIT license. See laravel/docs on GitHub for details.
The translated documentations are released under MIT license. See cornch/laravel-docs-l10n on GitHub for details.