简介

由于 HTTP 驱动的应用程序是无状态的,会话提供了一种在多个请求之间存储用户信息的方式。这些用户信息通常存储在持久化存储/后端中,以便后续请求能够访问。

Laravel 内置了多种会话后端,可通过简洁、统一的 API 进行访问。它支持诸如 Memcached、Redis 和数据库等流行后端。

配置

应用程序的会话配置文件位于 config/session.php。请务必仔细查看该文件中可供使用的选项。默认情况下,Laravel 配置使用 database 会话驱动。

会话的 driver 配置选项定义了每次请求时会话数据的存储位置。Laravel 包含以下几种驱动:

  • file - 会话存储在 storage/framework/sessions 目录中。
  • cookie - 会话存储在安全的加密 Cookie 中。
  • database - 会话存储在关系型数据库中。
  • memcached / redis - 会话存储在这两种基于缓存的快速存储中。
  • dynamodb - 会话存储在 AWS DynamoDB 中。
  • array - 会话存储在 PHP 数组中,不会被持久化。

array 驱动主要用于 测试,它可防止会话中存储的数据被持久化。

驱动前置条件

数据库

使用 database 会话驱动时,需要确保拥有一个用于存储会话数据的数据库表。通常,此表已包含在 Laravel 默认的 0001_01_01_000000_create_users_table.php 数据库迁移 文件中;但如果由于某种原因没有 sessions 表,可以使用 make:session-table Artisan 命令生成该迁移:

php artisan make:session-table

php artisan migrate

Redis

在 Laravel 中使用 Redis 会话前,需要通过 PECL 安装 PhpRedis PHP 扩展,或者通过 Composer 安装 predis/predis 包。有关 Redis 配置的更多信息,请参阅 Laravel 的 Redis 文档。

可以使用环境变量 SESSION_CONNECTION,或 session.php 配置文件中的 connection 选项,指定用于会话存储的 Redis 连接。

与会话交互

读取数据

在 Laravel 中处理会话数据主要有两种方式:全局 session 辅助函数和 Request 实例。首先,我们来看如何通过 Request 实例访问会话,可以在路由闭包或控制器方法中进行类型提示。请记住,控制器方法的依赖会通过 Laravel 服务容器 自动注入:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\View\View;

class UserController extends Controller
{
    /**
     * Show the profile for the given user.
     */
    public function show(Request $request, string $id): View
    {
        $value = $request->session()->get('key');

        // ...

        $user = $this->users->find($id);

        return view('user.profile', ['user' => $user]);
    }
}

从会话中获取数据项时,还可以将默认值作为第二个参数传递给 get 方法。如果会话中不存在指定的键,则返回该默认值。如果将闭包作为默认值传递给 get 方法,且请求的键不存在,则会执行该闭包并返回其结果:

$value = $request->session()->get('key', 'default');

$value = $request->session()->get('key', function () {
    return 'default';
});

全局会话辅助函数

也可以使用全局 session PHP 函数来读取和存储会话数据。当使用单个字符串参数调用 session 辅助函数时,它会返回该会话键对应的值。当使用键/值对数组调用该辅助函数时,这些值将被存储到会话中:

Route::get('/home', function () {
    // Retrieve a piece of data from the session...
    $value = session('key');

    // Specifying a default value...
    $value = session('key', 'default');

    // Store a piece of data in the session...
    session(['key' => 'value']);
});

通过 HTTP 请求实例使用会话,与使用全局 session 辅助函数之间几乎没有实际区别。这两种方法都可通过 测试,利用测试用例中可用的 assertSessionHas 方法进行断言。

获取全部会话数据

如果要获取会话中的所有数据,可以使用 all 方法:

$data = $request->session()->all();

获取部分会话数据

可以使用 only 和 except 方法获取会话数据的一个子集:

$data = $request->session()->only(['username', 'email']);

$data = $request->session()->except(['username', 'email']);

判断会话中是否存在某项数据

要判断会话中是否存在某项数据,可以使用 has 方法。如果该数据项存在且不为 null,has 方法将返回 true:

if ($request->session()->has('users')) {
    // ...
}

要判断会话中是否存在某项数据,即使其值为 null,可以使用 exists 方法:

if ($request->session()->exists('users')) {
    // ...
}

要判断会话中是否不存在某项数据,可以使用 missing 方法。如果该数据项不存在,missing 方法将返回 true:

if ($request->session()->missing('users')) {
    // ...
}

存储数据

要将数据存储到会话中,通常会使用请求实例的 put 方法或全局 session 辅助函数:

// Via a request instance...
$request->session()->put('key', 'value');

// Via the global "session" helper...
session(['key' => 'value']);

向数组型会话值追加数据

可以使用 push 方法向数组型会话值中追加新值。例如,如果 user.teams 键包含一个团队名称数组,可以像下面这样向数组中追加新值:

$request->session()->push('user.teams', 'developers');

读取并删除数据项

pull 方法可以在一条语句中从会话中读取并删除数据项:

$value = $request->session()->pull('key', 'default');

增减会话值

如果会话数据中包含希望增减的整数,可以使用 increment 和 decrement 方法:

$request->session()->increment('count');

$request->session()->increment('count', $incrementBy = 2);

$request->session()->decrement('count');

$request->session()->decrement('count', $decrementBy = 2);

一次性数据

有时可能需要将数据项存储到会话中,以便在下一次请求时使用。可以使用 flash 方法来实现。通过该方法存储到会话中的数据,会在当前请求和随后的 HTTP 请求中立即可用。在随后的 HTTP 请求结束后,这些一次性数据会被删除。一次性数据主要用于短生命周期的状态消息:

$request->session()->flash('status', 'Task was successful!');

如果需要让一次性数据在多次请求中保留,可以使用 reflash 方法,该方法会让所有一次性数据在下一次请求中继续保留。如果只需保留特定的一次性数据,可以使用 keep 方法:

$request->session()->reflash();

$request->session()->keep(['username', 'email']);

如果只想让一次性数据在当前请求中保留,可以使用 now 方法:

$request->session()->now('status', 'Task was successful!');

删除数据

forget 方法会从会话中移除一条数据。如果要删除会话中的所有数据,可以使用 flush 方法:

// 移除单个键值...
$request->session()->forget('name');

// 移除多个键值...
$request->session()->forget(['name', 'status']);

$request->session()->flush();

重新生成会话 ID

重新生成会话 ID 通常是为了防止恶意用户对应用程序发起会话固定攻击。

如果您正在使用 Laravel 的应用程序启动套件或 Laravel Fortify,Laravel 会在身份验证期间自动重新生成会话 ID;不过,如果您需要手动重新生成会话 ID,可以使用 regenerate 方法:

$request->session()->regenerate();

如果您需要在一个语句中重新生成会话 ID 并删除会话中的所有数据,可以使用 invalidate 方法:

$request->session()->invalidate();

会话缓存

Laravel 的会话缓存提供了一种便捷的方式,用于缓存作用域限定在单个用户会话的数据。与全局应用程序缓存不同,会话缓存数据会自动按会话隔离,并在会话过期或销毁时清除。会话缓存支持所有熟悉的Laravel 缓存方法,如 get、put、remember、forget 等,但作用域限定在当前会话。

会话缓存非常适合存储临时的、用户专属的数据,这些数据需要在同一会话内的多次请求中持久化,但无需永久存储。例如表单数据、临时计算结果、API 响应,或任何其他应与特定用户会话绑定的易失数据。

可以通过会话上的 cache 方法访问会话缓存:

$discount = $request->session()->cache()->get('discount');

$request->session()->cache()->put(
    'discount', 10, now()->plus(minutes: 5)
);

有关 Laravel 缓存方法的更多信息,请参阅缓存文档。

会话阻塞

要使用会话阻塞功能,您的应用程序必须使用支持原子锁的缓存驱动。目前,这些缓存驱动包括 memcached、dynamodb、redis、mongodb(包含在官方 mongodb/laravel-mongodb 包中)、database、file 和 array 驱动。此外,您不能使用 cookie 会话驱动。

默认情况下,Laravel 允许使用相同会话的请求并发执行。因此,例如,如果您使用 JavaScript HTTP 库向您的应用程序发送两个 HTTP 请求,它们将同时执行。对于许多应用程序来说,这并非问题;然而,在一小部分应用程序中,若并发向两个不同的应用端点发送请求,且这两个端点都会向会话写入数据,则可能会发生会话数据丢失的情况。

为解决这一问题,Laravel 提供了限制指定会话并发请求的功能。要开始使用,您只需在路由定义中链式调用 block 方法。在此示例中,对 /profile 端点的传入请求将获取会话锁。在持有该锁期间,任何使用相同会话 ID 发往 /profile 或 /order 端点的传入请求,都会等待第一个请求执行完毕后再继续执行:

Route::post('/profile', function () {
    // ...
})->block($lockSeconds = 10, $waitSeconds = 10);

Route::post('/order', function () {
    // ...
})->block($lockSeconds = 10, $waitSeconds = 10);

block 方法接受两个可选参数。block 方法接受的第一个参数是会话锁在释放前应持有的最长秒数。当然,如果请求在此之前执行完毕,锁会提前释放。

block 方法接受的第二个参数是请求在尝试获取会话锁时应等待的秒数。如果请求无法在给定秒数内获取会话锁,则会抛出 Illuminate\Contracts\Cache\LockTimeoutException。

如果未传入这两个参数,锁的最长获取时间为 10 秒,请求在尝试获取锁时的最长等待时间也为 10 秒:

Route::post('/profile', function () {
    // ...
})->block();

添加自定义会话驱动

实现驱动

如果现有的会话驱动都不符合您的应用程序需求,Laravel 支持您编写自定义会话处理器。您的自定义会话驱动应实现 PHP 内置的 SessionHandlerInterface。该接口仅包含几个简单方法。以下是 MongoDB 的桩实现示例:

<?php

namespace App\Extensions;

class MongoSessionHandler implements \SessionHandlerInterface
{
    public function open($savePath, $sessionName) {}
    public function close() {}
    public function read($sessionId) {}
    public function write($sessionId, $data) {}
    public function destroy($sessionId) {}
    public function gc($lifetime) {}
}

由于 Laravel 没有默认用于存放扩展的目录,您可以将其放置在任意位置。在此示例中,我们创建了一个 Extensions 目录来存放 MongoSessionHandler。

由于这些方法的用途并不直观,以下是每个方法的用途概述:

  • open 方法通常用于基于文件的会话存储系统。由于 Laravel 自带 file 会话驱动,您很少需要在此方法中添加任何内容,直接留空即可。
  • close 方法与 open 方法类似,通常也可忽略。对于大多数驱动而言,该方法并非必需。
  • read 方法应返回与给定 $sessionId 关联的会话数据的字符串版本。在驱动中检索或存储会话数据时,无需进行任何序列化或其他编码,因为 Laravel 会为您完成序列化操作。
  • write 方法应将与 $sessionId 关联的给定 $data 字符串写入持久化存储系统,如 MongoDB 或您选择的其他存储系统。同样,您无需执行任何序列化操作——Laravel 已经为您处理好了。
  • destroy 方法应从持久化存储中移除与 $sessionId 关联的数据。
  • gc 方法应销毁所有早于给定 $lifetime(单位为秒)的会话数据。对于 Memcached 和 Redis 等具有自过期功能的系统,该方法可留空。

注册驱动

驱动实现完成后,即可在 Laravel 中进行注册。要在 Laravel 的会话后端中添加额外驱动,您可以使用 Session 门面提供的 extend 方法。您应当在服务提供者的 boot 方法中调用 extend 方法。您可以在现有的 App\Providers\AppServiceProvider 中操作,也可以创建一个全新的服务提供者:

<?php

namespace App\Providers;

use App\Extensions\MongoSessionHandler;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\ServiceProvider;

class SessionServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        // ...
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        Session::extend('mongo', function (Application $app) {
            // Return an implementation of SessionHandlerInterface...
            return new MongoSessionHandler;
        });
    }
}

会话驱动注册完成后,您可以使用 SESSION_DRIVER 环境变量,或在应用程序的 config/session.php 配置文件中,将 mongo 驱动指定为应用程序的会话驱动。