跳到内容

会话

编辑此页

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 全局 app 变量 提供的 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 %}

通常使用 notice, warningerror 作为不同类型 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、Web 服务器指令或 .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

配置 Session 表和列名

用于存储 session 的表默认名为 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)
数据库中 session 表的名称;
db_username: (默认: '')
使用 PDO 配置时用于连接的用户名(当使用基于 DATABASE_URL 环境变量的连接时,它会覆盖在环境变量中定义的用户名)。
db_password: (默认: '')
使用 PDO 配置时用于连接的密码(当使用基于 DATABASE_URL 环境变量的连接时,它会覆盖在环境变量中定义的密码)。
db_id_col (默认 sess_id)
用于存储 session ID 的列的名称(列类型:VARCHAR(128));
db_data_col (默认 sess_data)
用于存储 session 数据的列的名称(列类型:BLOB);
db_time_col (默认 sess_time)
用于存储 session 创建时间戳的列的名称(列类型:INTEGER);
db_lifetime_col (默认 sess_lifetime)
用于存储 session 生命周期(lifetime)的列的名称(列类型:INTEGER);
db_connection_options (默认: [])
驱动程序特定的连接选项数组;
lock_mode (默认: LOCK_TRANSACTIONAL)
用于锁定数据库以避免竞态条件的策略。可能的值包括 LOCK_NONE(不锁定)、LOCK_ADVISORY(应用级别锁定)和 LOCK_TRANSACTIONAL(行级别锁定)。

准备数据库以存储 Session

在数据库中存储 session 之前,您必须创建用于存储信息的表。

如果安装了 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,用于在 MongoDB NoSQL 数据库中存储 session。首先,请确保您的 Symfony 应用程序中有一个可用的 MongoDB 连接,如 DoctrineMongoDBBundle 配置 文章中所述。

然后,为 MongoDbSessionHandler 注册一个新的处理程序服务,并将 MongoDB 连接作为参数传递给它,以及所需的参数

database:
数据库的名称
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 集合。但是,您可能需要添加索引以提高垃圾回收性能。从 MongoDB shell 运行此命令

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

配置 Session 字段名称

用于存储 session 的集合定义了某些字段名称。您可以使用传递给 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 生命周期(lifetime)的字段的名称。

在会话处理器之间迁移

如果您的应用程序更改了 session 的存储方式,请使用 MigratingSessionHandler 在旧的和新的保存处理程序之间迁移,而不会丢失 session 数据。

这是推荐的迁移工作流程

  1. 切换到迁移处理程序,并将您的新处理程序作为只写处理程序。旧的处理程序照常运行,session 将被写入新的处理程序。

    1
    $sessionStorage = new MigratingSessionHandler($oldSessionStorage, $newSessionStorage);
  2. 在您的 session 垃圾回收周期之后,验证新处理程序中的数据是否正确。
  3. 更新迁移处理程序以使用旧的处理程序作为只写处理程序,以便现在从新的处理程序读取 session。此步骤允许更轻松地回滚。

    1
    $sessionStorage = new MigratingSessionHandler($newSessionStorage, $oldSessionStorage);
  4. 在验证应用程序中的 session 正常工作后,从迁移处理程序切换到新的处理程序。

配置会话 TTL

默认情况下,Symfony 将使用 PHP 的 ini 设置 session.gc_maxlifetime 作为 session 生命周期。当您在数据库中存储 session 时,您还可以在框架配置中甚至在运行时配置自己的 TTL。

注意

一旦 session 启动,就无法更改 ini 设置,因此如果您想根据登录的用户使用不同的 TTL,则必须在运行时使用下面的回调方法来实现。

配置 TTL

您需要在您正在使用的 session 处理程序的 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 将 locale 设置存储在 Request 中,这意味着此设置不会在请求之间自动保存(“粘性”)。但是,您可以将 locale 存储在 session 中,以便在后续请求中使用它。

创建 LocaleSubscriber

创建一个新的事件订阅器。通常,_locale 用作路由参数来表示 locale,尽管您可以以任何您想要的方式确定正确的 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”路由和控制器),或者创建一个带有 _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]

现在,通过更改用户的 locale 并看到它在整个请求中保持粘性来庆祝一下。

请记住,要获取用户的 locale,始终使用 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();
}

根据用户偏好设置语言环境

您可能希望进一步改进此技术,并根据已登录用户的 user 实体定义 locale。但是,由于 LocaleSubscriberFirewallListener 之前被调用,而 FirewallListener 负责处理身份验证并在 TokenStorage 上设置用户 token,因此您无法访问已登录的用户。

假设您的 User 实体上有一个 locale 属性,并且希望将其用作给定用户的 locale。为了实现这一点,您可以挂钩到登录过程,并在用户被重定向到他们的第一个页面之前,使用此 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。

会话代理

session 代理机制有多种用途,本文演示了其中两个常见的用途。您可以不使用常规的 session 处理程序,而是通过定义一个扩展 SessionHandlerProxy 类的类来创建一个自定义的保存处理程序。

然后,将该类定义为一个服务。如果您正在使用默认的 services.yaml 配置,则会自动完成。

最后,使用 framework.session.handler_id 配置选项告诉 Symfony 使用您的 session 处理程序而不是默认的处理程序。

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

继续阅读下一节,了解如何在实践中使用 session 处理程序来解决两个常见的用例:加密 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 装饰此处理程序,例如 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 写入之前拦截它。

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 许可协议获得许可。
目录
    版本