跳到內容

工作階段

編輯此頁面

Symfony HttpFoundation 組件具有非常強大且彈性的工作階段子系統,旨在提供工作階段管理,您可以使用它透過清晰的物件導向介面,使用各種工作階段儲存驅動程式,來儲存關於使用者在請求之間的資訊。

Symfony 工作階段旨在取代使用 $_SESSION 超級全域變數和與操作工作階段相關的原生 PHP 函數,例如 session_start()session_regenerate_id()session_id()session_name()session_destroy()

注意

工作階段只有在您從中讀取或寫入時才會啟動。

安裝

您需要安裝 HttpFoundation 組件才能處理工作階段

1
$ composer require symfony/http-foundation

基本用法

工作階段可透過 Request 物件和 RequestStack 服務取得。如果您使用 RequestStack 類型提示引數,Symfony 會將 request_stack 服務注入到服務和控制器中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Symfony\Component\HttpFoundation\RequestStack;

class SomeService
{
    public function __construct(
        private RequestStack $requestStack,
    ) {
        // Accessing the session in the constructor is *NOT* recommended, since
        // it might not be accessible yet or lead to unwanted side-effects
        // $this->session = $requestStack->getSession();
    }

    public function someMethod(): void
    {
        $session = $this->requestStack->getSession();

        // ...
    }
}

從 Symfony 控制器,您也可以使用 Request 類型提示引數

1
2
3
4
5
6
7
8
9
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

public function index(Request $request): Response
{
    $session = $request->getSession();

    // ...
}

工作階段屬性

PHP 的工作階段管理需要使用 $_SESSION 超級全域變數。然而,這會干擾 OOP 範例中的程式碼可測試性和封裝性。為了協助克服此問題,Symfony 使用連結到工作階段的工作階段袋來封裝特定的屬性資料集。

這種方法減輕了 $_SESSION 超級全域變數中的命名空間污染,因為每個袋子都會將其所有資料儲存在唯一的命名空間下。這讓 Symfony 可以與可能使用 $_SESSION 超級全域變數的其他應用程式或程式庫和平共存,並且所有資料都保持與 Symfony 的工作階段管理完全相容。

工作階段袋是充當陣列的 PHP 物件

1
2
3
4
5
6
7
8
// stores an attribute for reuse during a later user request
$session->set('attribute-name', 'attribute-value');

// gets an attribute by name
$foo = $session->get('foo');

// the second argument is the value returned when the attribute doesn't exist
$filters = $session->get('filters', []);

儲存的屬性在該使用者工作階段的剩餘時間內保留在工作階段中。依預設,工作階段屬性是使用 AttributeBag 類別管理的鍵值對。

當您讀取、寫入,甚至檢查工作階段中資料是否存在時,工作階段會自動啟動。這可能會損害您的應用程式效能,因為所有使用者都會收到工作階段 Cookie。為了防止為匿名使用者啟動工作階段,您必須完全避免存取工作階段。

注意

當使用內部依賴工作階段的功能時,工作階段也會啟動,例如表單中的 CSRF 保護

Flash 訊息

您可以在使用者工作階段中儲存稱為「flash」訊息的特殊訊息。依照設計,「flash」訊息旨在僅使用一次:它們會在您檢索它們後立即自動從工作階段中消失。此功能使「flash」訊息特別適合儲存使用者通知。

例如,假設您正在處理表單提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
// ...

public function update(Request $request): Response
{
    // ...

    if ($form->isSubmitted() && $form->isValid()) {
        // do some sort of processing

        $this->addFlash(
            'notice',
            'Your changes were saved!'
        );
        // $this->addFlash() is equivalent to $request->getSession()->getFlashBag()->add()

        return $this->redirectToRoute(/* ... */);
    }

    return $this->render(/* ... */);
}

在處理請求後,控制器會在工作階段中設定 flash 訊息,然後重新導向。訊息金鑰(在本範例中為 notice)可以是任何內容。您將使用此金鑰來檢索訊息。

在下一個頁面的範本中(或甚至更好,在您的基本版面配置範本中),使用 Twig 全域應用程式變數提供的 flashes() 方法從工作階段讀取任何 flash 訊息。或者,您可以使用 peek() 方法來檢索訊息,同時將其保留在袋子中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{# templates/base.html.twig #}

{# read and display just one flash message type #}
{% for message in app.flashes('notice') %}
    <div class="flash-notice">
        {{ message }}
    </div>
{% endfor %}

{# same but without clearing them from the flash bag #}
{% for message in app.session.flashbag.peek('notice') %}
    <div class="flash-notice">
        {{ message }}
    </div>
{% endfor %}

{# read and display several types of flash messages #}
{% for label, messages in app.flashes(['success', 'warning']) %}
    {% for message in messages %}
        <div class="flash-{{ label }}">
            {{ message }}
        </div>
    {% endfor %}
{% endfor %}

{# read and display all flash messages #}
{% for label, messages in app.flashes %}
    {% for message in messages %}
        <div class="flash-{{ label }}">
            {{ message }}
        </div>
    {% endfor %}
{% endfor %}

{# or without clearing the flash bag #}
{% for label, messages in app.session.flashbag.peekAll() %}
    {% for message in messages %}
        <div class="flash-{{ label }}">
            {{ message }}
        </div>
    {% endfor %}
{% endfor %}

通常使用 noticewarningerror 作為不同類型 flash 訊息的金鑰,但您可以使用任何符合您需求的金鑰。

設定

在 Symfony 框架中,工作階段預設為啟用。工作階段儲存和其他設定可以在 config/packages/framework.yaml 中的 framework.session 設定下控制

1
2
3
4
5
6
7
8
9
10
11
12
# config/packages/framework.yaml
framework:
    # Enables session support. Note that the session will ONLY be started if you read or write from it.
    # Remove or comment this section to explicitly disable session support.
    session:
        # ID of the service used for session storage
        # NULL means that Symfony uses PHP default session mechanism
        handler_id: null
        # improves the security of the cookies used for sessions
        cookie_secure: auto
        cookie_samesite: lax
        storage_factory_id: session.storage.factory.native

handler_id 設定選項設定為 null 表示 Symfony 將使用原生 PHP 工作階段機制。工作階段中繼資料檔案將儲存在 Symfony 應用程式外部,在由 PHP 控制的目錄中。雖然這通常簡化了事情,但如果寫入相同目錄的其他應用程式具有較短的最大存留期設定,則某些與工作階段到期相關的選項可能無法如預期般運作。

如果您願意,您可以使用 session.handler.native_file 服務作為 handler_id,讓 Symfony 自己管理工作階段。另一個有用的選項是 save_path,它定義了 Symfony 將儲存工作階段中繼資料檔案的目錄

1
2
3
4
5
6
# config/packages/framework.yaml
framework:
    session:
        # ...
        handler_id: 'session.handler.native_file'
        save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'

查看 Symfony 設定參考,以了解更多關於其他可用的 工作階段設定選項

警告

Symfony 工作階段與 php.ini 指令 session.auto_start = 1 不相容。此指令應在 php.ini、網頁伺服器指令或 .htaccess 中關閉。

7.2

sid_lengthsid_bits_per_character 選項在 Symfony 7.2 中已棄用,並將在 Symfony 8.0 中忽略。

工作階段 Cookie 也可在 Response 物件中使用。這在 CLI 環境中或使用 Roadrunner 或 Swoole 等 PHP 執行器時取得該 Cookie 非常有用。

工作階段閒置時間/保持活動

在許多情況下,當使用者離開終端機並登入時,您可能想要保護工作階段或將未經授權的使用降至最低,方法是在閒置一段時間後銷毀工作階段。例如,銀行應用程式通常會在閒置 5 到 10 分鐘後將使用者登出。在這裡設定 Cookie 的生命週期是不適當的,因為這可以由用戶端操縱,因此我們必須在伺服器端進行到期設定。最簡單的方法是透過合理頻繁執行的工作階段垃圾回收來實作此功能。cookie_lifetime 將設定為相對較高的值,而垃圾回收 gc_maxlifetime 將設定為在任何所需的閒置時間段銷毀工作階段。

另一個選項是在工作階段啟動後,明確檢查工作階段是否已過期。可以根據需要銷毀工作階段。這種處理方法可以讓工作階段到期整合到使用者體驗中,例如,透過顯示訊息。

Symfony 記錄關於每個工作階段的一些中繼資料,以便讓您精細控制安全性設定

1
2
$session->getMetadataBag()->getCreated();
$session->getMetadataBag()->getLastUsed();

兩種方法都傳回 Unix 時間戳記(相對於伺服器)。

此中繼資料可用於在存取時明確使工作階段過期

1
2
3
4
5
$session->start();
if (time() - $session->getMetadataBag()->getLastUsed() > $maxIdleTime) {
    $session->invalidate();
    throw new SessionExpired(); // redirect to expired session page
}

也可以透過讀取 getLifetime() 方法來判斷特定 Cookie 的 cookie_lifetime 設定為何

1
$session->getMetadataBag()->getLifetime();

Cookie 的到期時間可以透過將建立時間戳記和生命週期相加來確定。

設定垃圾回收

當工作階段開啟時,PHP 會根據 session.gc_probability / session.gc_divisor 設定的機率隨機呼叫 gc 處理器。例如,如果這些分別設定為 5/100,則表示機率為 5%。同樣地,3/4 表示被呼叫的機率為 3/4,即 75%。

如果垃圾回收處理器被調用,PHP 將傳遞儲存在 php.ini 指令 session.gc_maxlifetime 中的值。在此環境中,其含義是任何儲存超過 gc_maxlifetime 時間的工作階段都應刪除。這允許人們根據閒置時間使記錄過期。

但是,某些作業系統(例如 Debian)以不同的方式管理工作階段處理,並將 session.gc_probability 變數設定為 0,以防止 PHP 執行垃圾回收。依預設,Symfony 使用 php.ini 檔案中設定的 gc_probability 指令的值。如果您無法修改此 PHP 設定,您可以直接在 Symfony 中設定它

1
2
3
4
5
# config/packages/framework.yaml
framework:
    session:
        # ...
        gc_probability: 1

或者,您可以透過將 gc_probabilitygc_divisorgc_maxlifetime 作為陣列傳遞給 NativeSessionStorage 的建構函式或 setOptions() 方法來設定這些設定。

7.2

在 Symfony 7.2 中引入了使用 php.ini 指令作為 gc_probability 的預設值。

將工作階段儲存在資料庫中

Symfony 預設將工作階段儲存在檔案中。如果您的應用程式由多個伺服器提供服務,您將需要改用資料庫,以使工作階段跨不同伺服器運作。

Symfony 可以將工作階段儲存在各種資料庫(關聯式、NoSQL 和鍵值)中,但建議使用 Redis 等鍵值資料庫以獲得最佳效能。

將工作階段儲存在鍵值資料庫 (Redis) 中

本節假設您已擁有完全運作的 Redis 伺服器,並且也已安裝和設定 phpredis 擴充功能

您有兩個不同的選項可以使用 Redis 儲存工作階段

第一個基於 PHP 的選項是在伺服器 php.ini 檔案中直接設定 Redis 工作階段處理器

1
2
3
; php.ini
session.save_handler = redis
session.save_path = "tcp://192.168.0.178:6379?auth=REDIS_PASSWORD"

第二個選項是在 Symfony 中設定 Redis 工作階段。首先,為與 Redis 伺服器的連線定義一個 Symfony 服務

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# config/services.yaml
services:
    # ...
    Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler:
        arguments:
            - '@Redis'
            # you can optionally pass an array of options. The only options are 'prefix' and 'ttl',
            # which define the prefix to use for the keys to avoid collision on the Redis server
            # and the expiration time for any given entry (in seconds), defaults are 'sf_s' and null:
            # - { 'prefix': 'my_prefix', 'ttl': 600 }

    Redis:
        # you can also use \RedisArray, \RedisCluster, \Relay\Relay or \Predis\Client classes
        class: \Redis
        calls:
            - connect:
                - '%env(REDIS_HOST)%'
                - '%env(int:REDIS_PORT)%'

            # uncomment the following if your Redis server requires a password
            # - auth:
            #     - '%env(REDIS_PASSWORD)%'

            # uncomment the following if your Redis server requires a user and a password (when user is not default)
            # - auth:
            #     - ['%env(REDIS_USER)%','%env(REDIS_PASSWORD)%']

接下來,使用 handler_id 設定選項來告知 Symfony 使用此服務作為工作階段處理器

1
2
3
4
5
# config/packages/framework.yaml
framework:
    # ...
    session:
        handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler

Symfony 現在將使用您的 Redis 伺服器來讀取和寫入工作階段資料。此解決方案的主要缺點是 Redis 不執行工作階段鎖定,因此在存取工作階段時可能會遇到競爭條件。例如,您可能會看到「無效的 CSRF 權杖」錯誤,因為兩個請求是並行發出的,並且只有第一個請求將 CSRF 權杖儲存在工作階段中。

另請參閱

如果您使用 Memcached 而不是 Redis,請遵循類似的方法,但將 RedisSessionHandler 替換為 MemcachedSessionHandler

提示

當在 handler_id 設定選項中使用帶有 DSN 的 Redis 時,您可以將 prefixttl 選項作為 DSN 中的查詢字串參數新增。

將工作階段儲存在關聯式資料庫 (MariaDB、MySQL、PostgreSQL) 中

Symfony 包含 PdoSessionHandler,用於將工作階段儲存在 MariaDB、MySQL 和 PostgreSQL 等關聯式資料庫中。若要使用它,請先使用您的資料庫認證註冊新的處理器服務

1
2
3
4
5
6
7
8
9
10
11
# config/services.yaml
services:
    # ...

    Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
        arguments:
            - '%env(DATABASE_URL)%'

            # you can also use PDO configuration, but requires passing two arguments
            # - 'mysql:dbname=mydatabase; host=myhost; port=myport'
            # - { db_username: myuser, db_password: mypassword }

提示

當使用 MySQL 作為資料庫時,在 DATABASE_URL 中定義的 DSN 可以包含 charsetunix_socket 選項作為查詢字串參數。

接下來,使用 handler_id 設定選項來告知 Symfony 使用此服務作為工作階段處理器

1
2
3
4
5
# config/packages/framework.yaml
framework:
    session:
        # ...
        handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler

設定工作階段資料表和欄位名稱

用於儲存工作階段的資料表預設名為 sessions,並定義了某些欄位名稱。您可以使用傳遞給 PdoSessionHandler 服務的第二個引數來設定這些值

1
2
3
4
5
6
7
8
# config/services.yaml
services:
    # ...

    Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
        arguments:
            - '%env(DATABASE_URL)%'
            - { db_table: 'customer_session', db_id_col: 'guid' }

以下是您可以設定的參數

db_table(預設 sessions
資料庫中工作階段資料表的名稱;
db_username:(預設值:''
使用 PDO 設定時使用的使用者名稱(當使用基於 DATABASE_URL 環境變數的連線時,它會覆寫環境變數中定義的使用者名稱)。
db_password:(預設值:''
使用 PDO 設定時使用的密碼(當使用基於 DATABASE_URL 環境變數的連線時,它會覆寫環境變數中定義的密碼)。
db_id_col(預設 sess_id
儲存工作階段 ID 的欄位名稱(欄位類型:VARCHAR(128));
db_data_col(預設 sess_data
儲存工作階段資料的欄位名稱(欄位類型:BLOB);
db_time_col(預設 sess_time
儲存工作階段建立時間戳記的欄位名稱(欄位類型:INTEGER);
db_lifetime_col(預設 sess_lifetime
儲存工作階段生命週期的欄位名稱(欄位類型:INTEGER);
db_connection_options(預設值:[]
驅動程式特定的連線選項陣列;
lock_mode(預設值:LOCK_TRANSACTIONAL
鎖定資料庫以避免競爭條件的策略。可能的值為 LOCK_NONE(不鎖定)、LOCK_ADVISORY(應用程式層級鎖定)和 LOCK_TRANSACTIONAL(資料列層級鎖定)。

準備資料庫以儲存工作階段

在資料庫中儲存工作階段之前,您必須建立儲存資訊的資料表。

安裝 Doctrine 後,如果您 Doctrine 目標資料庫與此組件使用的資料庫相同,當您執行 make:migration 命令時,將會自動產生 session 資料表。

或者,如果您偏好自行建立資料表,且該資料表尚未建立,session 處理器提供了一個名為 createTable() 的方法,可根據您使用的資料庫引擎為您設定此資料表

1
2
3
4
5
try {
    $sessionHandlerService->createTable();
} catch (\PDOException $exception) {
    // the table could not be created for some reason
}

如果資料表已存在,將會拋出例外。

如果您寧願自行設定資料表,建議使用以下命令產生一個空的資料庫遷移檔

1
$ php bin/console doctrine:migrations:generate

然後,在下方找到適用於您資料庫的 SQL,將其新增至遷移檔案並使用以下命令執行遷移

1
$ php bin/console doctrine:migrations:migrate

如果需要,您也可以透過在程式碼中呼叫 configureSchema() 方法,將此資料表新增至您的 schema。

MariaDB/MySQL
1
2
3
4
5
6
7
CREATE TABLE `sessions` (
    `sess_id` VARBINARY(128) NOT NULL PRIMARY KEY,
    `sess_data` BLOB NOT NULL,
    `sess_lifetime` INTEGER UNSIGNED NOT NULL,
    `sess_time` INTEGER UNSIGNED NOT NULL,
    INDEX `sessions_sess_lifetime_idx` (`sess_lifetime`)
) COLLATE utf8mb4_bin, ENGINE = InnoDB;

注意

BLOB 欄位類型(createTable() 預設使用的類型)最多可儲存 64 kb。如果使用者 session 資料超過此限制,可能會拋出例外,或他們的 session 將會被靜默重設。如果您需要更多空間,請考慮使用 MEDIUMBLOB

PostgreSQL
1
2
3
4
5
6
7
CREATE TABLE sessions (
    sess_id VARCHAR(128) NOT NULL PRIMARY KEY,
    sess_data BYTEA NOT NULL,
    sess_lifetime INTEGER NOT NULL,
    sess_time INTEGER NOT NULL
);
CREATE INDEX sessions_sess_lifetime_idx ON sessions (sess_lifetime);
Microsoft SQL Server
1
2
3
4
5
6
7
CREATE TABLE sessions (
    sess_id VARCHAR(128) NOT NULL PRIMARY KEY,
    sess_data NVARCHAR(MAX) NOT NULL,
    sess_lifetime INTEGER NOT NULL,
    sess_time INTEGER NOT NULL,
    INDEX sessions_sess_lifetime_idx (sess_lifetime)
);

將工作階段儲存在 NoSQL 資料庫 (MongoDB) 中

Symfony 包含一個 MongoDbSessionHandler,用於將 session 儲存在 MongoDB NoSQL 資料庫中。首先,請確保您的 Symfony 應用程式中已建立可運作的 MongoDB 連線,如 DoctrineMongoDBBundle 設定 文章中所述。

然後,為 MongoDbSessionHandler 註冊一個新的 handler 服務,並將 MongoDB 連線作為參數傳遞給它,以及必要的參數

database:
資料庫的名稱
collection:
collection 的名稱
1
2
3
4
5
6
7
8
# config/services.yaml
services:
    # ...

    Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler:
        arguments:
            - '@doctrine_mongodb.odm.default_connection'
            - { database: '%env(MONGODB_DB)%', collection: 'sessions' }

接下來,使用 handler_id 設定選項來告知 Symfony 使用此服務作為工作階段處理器

1
2
3
4
5
# config/packages/framework.yaml
framework:
    session:
        # ...
        handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler

就這樣!Symfony 現在將使用您的 MongoDB 伺服器來讀取和寫入 session 資料。您不需要執行任何操作來初始化您的 session collection。但是,您可能想要新增索引以提高垃圾回收效能。從 MongoDB shell 執行此操作

1
2
use session_db
db.session.createIndex( { "expires_at": 1 }, { expireAfterSeconds: 0 } )

設定 Session 欄位名稱

用於儲存 session 的 collection 定義了某些欄位名稱。您可以使用傳遞給 MongoDbSessionHandler 服務的第二個參數來設定這些值

1
2
3
4
5
6
7
8
9
10
11
12
# config/services.yaml
services:
    # ...

    Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler:
        arguments:
            - '@doctrine_mongodb.odm.default_connection'
            -
                database: '%env(MONGODB_DB)%'
                collection: 'sessions'
                id_field: '_guid'
                expiry_field: 'eol'

以下是您可以設定的參數

id_field (預設 _id)
用於儲存 session ID 的欄位名稱;
data_field (預設 data)
用於儲存 session 資料的欄位名稱;
time_field (預設 time)
用於儲存 session 建立時間戳記的欄位名稱;
expiry_field (預設 expires_at)
用於儲存 session 生存時間的欄位名稱。

在工作階段處理器之間遷移

如果您的應用程式變更了 session 的儲存方式,請使用 MigratingSessionHandler 在新舊儲存處理器之間遷移,而不會遺失 session 資料。

這是建議的遷移工作流程

  1. 切換到 migrating handler,並將您的新 handler 作為僅寫入的 handler。舊的 handler 照常運作,而 session 會寫入到新的 handler

    1
    $sessionStorage = new MigratingSessionHandler($oldSessionStorage, $newSessionStorage);
  2. 在您的 session 垃圾回收期間之後,驗證新 handler 中的資料是否正確。
  3. 更新 migrating handler 以使用舊的 handler 作為僅寫入的 handler,以便現在從新的 handler 讀取 session。此步驟允許更輕鬆地回滾

    1
    $sessionStorage = new MigratingSessionHandler($newSessionStorage, $oldSessionStorage);
  4. 在驗證您的應用程式中的 session 運作正常後,從 migrating handler 切換到新的 handler。

設定工作階段 TTL

Symfony 預設會使用 PHP 的 ini 設定 session.gc_maxlifetime 作為 session 生存時間。當您將 session 儲存在資料庫中時,您也可以在框架設定中或甚至在執行時設定自己的 TTL。

注意

一旦 session 開始,就無法變更 ini 設定,因此如果您想要根據登入的使用者使用不同的 TTL,您必須在執行時使用以下回呼方法執行此操作。

設定 TTL

您需要在您正在使用的 session handler 的 options 陣列中傳遞 TTL

1
2
3
4
5
6
7
# config/services.yaml
services:
    # ...
    Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler:
        arguments:
            - '@Redis'
            - { 'ttl': 600 }

在執行時動態設定 TTL

如果您希望針對不同的使用者或 session 擁有不同的 TTL,無論基於何種原因,這也是可行的,方法是傳遞一個回呼作為 TTL 值。回呼將在 session 寫入之前被呼叫,並且必須傳回一個將用作 TTL 的整數。

1
2
3
4
5
6
7
8
9
10
11
12
13
# config/services.yaml
services:
    # ...
    Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler:
        arguments:
            - '@Redis'
            - { 'ttl': !closure '@my.ttl.handler' }

    my.ttl.handler:
        class: Some\InvokableClass # some class with an __invoke() method
        arguments:
            # Inject whatever dependencies you need to be able to resolve a TTL for the current session
            - '@security'

在使用者工作階段期間讓語系「保持黏著性」

Symfony 將語系設定儲存在 Request 中,這表示此設定不會自動在請求之間儲存 ("sticky")。但是,您可以將語系儲存在 session 中,以便在後續請求中使用。

建立 LocaleSubscriber

建立一個新的事件訂閱器。通常,_locale 用作路由參數以表示語系,儘管您可以隨意決定正確的語系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// src/EventSubscriber/LocaleSubscriber.php
namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class LocaleSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private string $defaultLocale = 'en',
    ) {
    }

    public function onKernelRequest(RequestEvent $event): void
    {
        $request = $event->getRequest();
        if (!$request->hasPreviousSession()) {
            return;
        }

        // try to see if the locale has been set as a _locale routing parameter
        if ($locale = $request->attributes->get('_locale')) {
            $request->getSession()->set('_locale', $locale);
        } else {
            // if no explicit locale has been set on this request, use one from the session
            $request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
        }
    }

    public static function getSubscribedEvents(): array
    {
        return [
            // must be registered before (i.e. with a higher priority than) the default Locale listener
            KernelEvents::REQUEST => [['onKernelRequest', 20]],
        ];
    }
}

如果您使用預設的 services.yaml 設定,您就完成了!Symfony 將自動知道事件訂閱器,並在每個請求上呼叫 onKernelRequest 方法。

若要查看其運作方式,請手動在 session 上設定 _locale 鍵(例如,透過某些 "變更語系" 路由 & 控制器),或建立一個具有 _locale 預設值的路由。

您也可以明確地設定它,以便傳遞 default_locale

1
2
3
4
5
6
7
8
# config/services.yaml
services:
    # ...

    App\EventSubscriber\LocaleSubscriber:
        arguments: ['%kernel.default_locale%']
        # uncomment the next line if you are not using autoconfigure
        # tags: [kernel.event_subscriber]

現在,變更使用者的語系並查看它在整個請求中是否保持不變,以示慶祝。

請記住,若要取得使用者的語系,請始終使用 Request::getLocale 方法

1
2
3
4
5
6
7
// from a controller...
use Symfony\Component\HttpFoundation\Request;

public function index(Request $request): void
{
    $locale = $request->getLocale();
}

根據使用者偏好設定語系

您可能想要進一步改進此技術,並根據已登入使用者的使用者實體定義語系。但是,由於 LocaleSubscriberFirewallListener 之前被呼叫,而 FirewallListener 負責處理驗證並在 TokenStorage 上設定使用者 token,因此您無法存取已登入的使用者。

假設您的 User 實體上具有 locale 屬性,並希望將其用作給定使用者的語系。若要完成此操作,您可以掛鉤到登入程序,並在使用者被重新導向到他們的第一個頁面之前,使用此語系值更新使用者的 session。

若要執行此操作,您需要在 LoginSuccessEvent::class 事件上建立一個事件訂閱器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// src/EventSubscriber/UserLocaleSubscriber.php
namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;

/**
 * Stores the locale of the user in the session after the
 * login. This can be used by the LocaleSubscriber afterwards.
 */
class UserLocaleSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private RequestStack $requestStack,
    ) {
    }

    public function onLoginSuccess(LoginSuccessEvent $event): void
    {
        $user = $event->getUser();

        if (null !== $user->getLocale()) {
            $this->requestStack->getSession()->set('_locale', $user->getLocale());
        }
    }

    public static function getSubscribedEvents(): array
    {
        return [
            LoginSuccessEvent::class => 'onLoginSuccess',
        ];
    }
}

警告

為了在使用者變更其語言偏好設定後立即更新語言,您還需要在變更 User 實體時更新 session。

工作階段 Proxy

session 代理機制有多種用途,本文示範了兩個常見的用途。您可以建立一個自訂的儲存處理器,方法是定義一個擴展 SessionHandlerProxy 類別的類別,而不是使用常規的 session handler。

然後,將該類別定義為一個 服務。如果您使用預設的 services.yaml 設定,則會自動發生。

最後,使用 framework.session.handler_id 設定選項來告訴 Symfony 使用您的 session handler 而不是預設的 handler

1
2
3
4
5
# config/packages/framework.yaml
framework:
    session:
        # ...
        handler_id: App\Session\CustomSessionHandler

繼續閱讀接下來的章節,以了解如何在實務中使用 session handler 來解決兩個常見的使用案例:加密 session 資訊和定義唯讀訪客 session。

工作階段資料加密

如果您想要加密 session 資料,您可以使用代理來根據需要加密和解密 session。以下範例使用 php-encryption 程式庫,但您可以將其調整為您可能正在使用的任何其他程式庫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// src/Session/EncryptedSessionProxy.php
namespace App\Session;

use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;

class EncryptedSessionProxy extends SessionHandlerProxy
{
    public function __construct(
        private \SessionHandlerInterface $handler,
        private Key $key
    ) {
        parent::__construct($handler);
    }

    public function read($id): string
    {
        $data = parent::read($id);

        return Crypto::decrypt($data, $this->key);
    }

    public function write($id, $data): string
    {
        $data = Crypto::encrypt($data, $this->key);

        return parent::write($id, $data);
    }
}

加密 session 資料的另一種可能性是裝飾 session.marshaller 服務,該服務指向 MarshallingSessionHandler。您可以使用使用加密的 marshaller 來裝飾此 handler,例如 SodiumMarshaller

首先,您需要產生一個安全金鑰,並將其作為 SESSION_DECRYPTION_FILE 新增到您的 secret store

1
$ php -r 'echo base64_encode(sodium_crypto_box_keypair());'

然後,使用此金鑰註冊 SodiumMarshaller 服務

1
2
3
4
5
6
7
8
9
# config/services.yaml
services:

    # ...
    Symfony\Component\Cache\Marshaller\SodiumMarshaller:
        decorates: 'session.marshaller'
        arguments:
            - ['%env(file:resolve:SESSION_DECRYPTION_FILE)%']
            - '@.inner'

危險

這將加密快取項目的值,但不會加密快取金鑰。請注意不要在金鑰中洩漏敏感資料。

唯讀訪客工作階段

在某些應用程式中,訪客使用者需要 session,但沒有特別需要持久化 session。在這種情況下,您可以在 session 寫入之前攔截 session

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// src/Session/ReadOnlySessionProxy.php
namespace App\Session;

use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;

class ReadOnlySessionProxy extends SessionHandlerProxy
{
    public function __construct(
        private \SessionHandlerInterface $handler,
        private Security $security
    ) {
        parent::__construct($handler);
    }

    public function write($id, $data): string
    {
        if ($this->getUser() && $this->getUser()->isGuest()) {
            return;
        }

        return parent::write($id, $data);
    }

    private function getUser(): ?User
    {
        $user = $this->security->getUser();
        if (is_object($user)) {
            return $user;
        }

        return null;
    }
}

與舊版應用程式整合

如果您正在將 Symfony 完整堆疊框架整合到使用 session_start() 啟動 session 的舊版應用程式中,您仍然可以透過使用 PHP Bridge session 來使用 Symfony 的 session 管理。

如果應用程式有自己的 PHP 儲存處理器,您可以為 handler_id 指定 null

1
2
3
4
5
# config/packages/framework.yaml
framework:
    session:
        storage_factory_id: session.storage.factory.php_bridge
        handler_id: ~

否則,如果問題是您無法避免應用程式使用 session_start() 啟動 session,您仍然可以透過指定儲存處理器(如下例所示)來使用基於 Symfony 的 session 儲存處理器

1
2
3
4
5
# config/packages/framework.yaml
framework:
    session:
        storage_factory_id: session.storage.factory.php_bridge
        handler_id: session.handler.native_file

注意

如果舊版應用程式需要自己的 session 儲存處理器,請不要覆寫它。而是設定 handler_id: ~。請注意,一旦 session 啟動,就無法變更儲存處理器。如果應用程式在 Symfony 初始化之前啟動 session,則儲存處理器將已設定。在這種情況下,您將需要 handler_id: ~。僅當您確定舊版應用程式可以使用 Symfony 儲存處理器而不會產生副作用,並且 session 在 Symfony 初始化之前尚未啟動時,才覆寫儲存處理器。

本作品,包含程式碼範例,依據 Creative Commons BY-SA 3.0 授權條款授權。
TOC
    版本