HTTP Session
簡介
由於使用 HTTP 的應用程式是無狀態的 (Stateless),因此 Session 提供了能在多個 Request 間儲存有關使用者資訊的方法。這個使用者資訊通常儲存於持續性存放空間 (Persistent Store) 或後端中,能讓我們在之後的 Request 中存取。
Laravel 隨附了多種 Session 後端,能讓我們使用直觀且同一的 API 來存取 Session。支援的後端包含常見的 Memcached、Redis、與資料庫。
設定
Your application's session configuration file is stored at config/session.php
. Be sure to review the options available to you in this file. By default, Laravel is configured to use the database
session driver.
The session driver
configuration option defines where session data will be stored for each request. Laravel includes a variety of drivers:
-
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
When using the database
session driver, you will need to ensure that you have a database table to contain the session data. Typically, this is included in Laravel's default 0001_01_01_000000_create_users_table.php
database migration; however, if for any reason you do not have a sessions
table, you may use the make:session-table
Artisan command to generate this migration:
1php artisan make:session-table23php artisan migrate
1php artisan make:session-table23php artisan migrate
Redis
在 Laravel 上使用 Redis Session 前,必須先使用 PECL 安裝 PhpRedis PHP 擴充程式,或是使用 Composer 安裝 predis/predis
套件 (~1.0)。更多有關設定 Redis 的資訊,請參考 Laravel 的 Redis 說明文件。
The SESSION_CONNECTION
environment variable, or the connection
option in the session.php
configuration file, may be used to specify which Redis connection is used for session storage.
Interacting With the Session
取得資料
在 Laravel 中有兩種使用 Session 的方式:全域 session
輔助函式,或是 Request
實體。首先,我們先來看看如何使用 Request
實體來存取 Session。可以在 Route 閉包或 Controller 方法上型別提示 (Type-Hint) Request。請記住,Controller 方法的相依性會自動由 Laravel 的 Service Container 插入:
1<?php23namespace App\Http\Controllers;45use Illuminate\Http\Request;6use Illuminate\View\View;78class UserController extends Controller9{10 /**11 * Show the profile for the given user.12 */13 public function show(Request $request, string $id): View14 {15 $value = $request->session()->get('key');1617 // ...1819 $user = $this->users->find($id);2021 return view('user.profile', ['user' => $user]);22 }23}
1<?php23namespace App\Http\Controllers;45use Illuminate\Http\Request;6use Illuminate\View\View;78class UserController extends Controller9{10 /**11 * Show the profile for the given user.12 */13 public function show(Request $request, string $id): View14 {15 $value = $request->session()->get('key');1617 // ...1819 $user = $this->users->find($id);2021 return view('user.profile', ['user' => $user]);22 }23}
從 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 // Retrieve a piece of data from the session...3 $value = session('key');45 // Specifying a default value...6 $value = session('key', 'default');78 // Store a piece of data in the session...9 session(['key' => 'value']);10});
1Route::get('/home', function () {2 // Retrieve a piece of data from the session...3 $value = session('key');45 // Specifying a default value...6 $value = session('key', 'default');78 // Store a piece of data in the session...9 session(['key' => 'value']);10});
使用 HTTP Request 實體跟全域 session
輔助函式在實務上沒有太大的不同。不管是哪種方式都是可測試的,測試時可以使用測試例中的 assertSessionHas
方法來測試。
取得所有 Session 資料
若想從 Session 中取得所有資料,可以使用 all
方法:
1$data = $request->session()->all();
1$data = $request->session()->all();
Retrieving a Portion of the Session Data
The only
and except
methods may be used to retrieve a subset of the session data:
1$data = $request->session()->only(['username', 'email']);23$data = $request->session()->except(['username', 'email']);
1$data = $request->session()->only(['username', 'email']);23$data = $request->session()->except(['username', 'email']);
Determining if an Item Exists in the 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// Via a request instance...2$request->session()->put('key', 'value');34// Via the global "session" helper...5session(['key' => 'value']);
1// Via a request instance...2$request->session()->put('key', 'value');34// Via the global "session" helper...5session(['key' => 'value']);
Pushing to Array Session Values
可以使用 push
方法來將值推入 (Push) 到陣列的 Session 值中。舉例來說,若 user.teams
索引鍵中包含了一組團隊名稱陣列,我們可以像這樣將一個新的值推入陣列中:
1$request->session()->push('user.teams', 'developers');
1$request->session()->push('user.teams', 'developers');
Retrieving and Deleting an Item
使用 pull
方法即可以單一陳述式從 Session 內取得並刪除某個項目:
1$value = $request->session()->pull('key', 'default');
1$value = $request->session()->pull('key', 'default');
Incrementing and Decrementing Session Values
若 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// Forget a single key...2$request->session()->forget('name');34// Forget multiple keys...5$request->session()->forget(['name', 'status']);67$request->session()->flush();
1// Forget a single key...2$request->session()->forget('name');34// Forget multiple keys...5$request->session()->forget(['name', 'status']);67$request->session()->flush();
Regenerating the 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 封鎖
To utilize session blocking, your application must be using a cache driver that supports atomic locks. Currently, those cache drivers include the memcached
, dynamodb
, redis
, database
, file
, and array
drivers. In addition, you may not use the 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
Implementing the 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 會幫你序列化。 - The
write
method should write the given$data
string associated with the$sessionId
to some persistent storage system, such as MongoDB or another storage system of your choice. Again, you should not perform any serialization - Laravel will have already handled that for you. -
destroy
方法從持續性儲存系統中移除任何與$sessionId
關聯的資料。 -
gc
方法移除所有時間舊於$lifetime
的Session
資料。$lifetime
是 UNIX 時戳。對於自帶有效期限的系統,如 Memcached 或 Redis,可以將這個方法留空。
Registering the 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 // Return an implementation of 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 // Return an implementation of SessionHandlerInterface...27 return new MongoSessionHandler;28 });29 }30}
Once the session driver has been registered, you may specify the mongo
driver as your application's session driver using the SESSION_DRIVER
environment variable or within the application's config/session.php
configuration file.