Http 会话

介绍

由于基于 HTTP 的应用程序是无状态的,Session 提供了一种在多个请求之间存储用户信息的方式。这些用户信息通常会被放置在一个持久化存储后端中,并可以从后续的请求中访问。

Laravel 提供了多种会话后端,可以通过简洁且统一的 API 进行访问。包括对 MemcachedRedis 和数据库等流行后端的支持。

配置

您的应用程序的会话配置文件存储在 config/session.php 中。务必检查此文件中提供的选项。默认情况下,Laravel 配置为使用 database 会话驱动程序。

session 驱动程序配置选项定义了每次请求时会话数据的存储位置。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 数据库迁移中;但是,如果由于某些原因您没有会话表,可以使用 make:session-table Artisan 命令生成此迁移:

php artisan make:session-table

php artisan migrate

Redis

在 Laravel 中使用 Redis 会话之前,您需要通过 PECL 安装 PhpRedis PHP 扩展,或者通过 Composer 安装 predis/predis 包(~1.0)。有关 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 助手函数

您也可以使用全局的 session PHP 函数来检索和存储会话数据。当调用 session 助手函数时,如果传递一个字符串参数,它将返回该会话键的值。如果调用时传递一个键/值对数组,这些值将会存储在会话中:

Route::get('/home', function () {
    // 从会话中检索数据...
    $value = session('key');

    // 指定默认值...
    $value = session('key', 'default');

    // 存储数据到会话中...
    session(['key' => 'value']);
});

使用 HTTP 请求实例与使用全局 session 助手函数在实践中的差别不大。两者都可以通过 assertSessionHas 方法进行【测试】,该方法在所有测试用例中都可用。

检索所有会话数据

如果您希望检索会话中的所有数据,可以使用 all 方法:

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

检索部分会话数据

您可以使用 onlyexcept 方法来检索会话数据的子集:

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

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

确定会话项是否存在

要确定会话中是否存在某个项,您可以使用 has 方法。has 方法在该项存在并且不为 null 时返回 true

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

要确定会话中是否存在某个项,即使它的值为 null,您可以使用 exists 方法:

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

要确定会话中某项是否不存在,您可以使用 missing 方法。missing 方法在该项不存在时返回 true

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

存储数据

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

// 通过请求实例...
$request->session()->put('key', 'value');

// 通过全局 "session" 助手函数...
session(['key' => 'value']);

向数组类型的会话值推送数据

push 方法可用于将新值推送到会话值为数组的项中。例如,如果 user.teams 键包含一个团队名称的数组,您可以这样将新值推送到数组中:

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

检索和删除会话项

pull 方法会在检索会话项的同时将其从会话中删除:

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

增加和减少会话值

如果会话数据中包含整数,您希望对其进行递增或递减,可以使用 incrementdecrement 方法:

$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();

会话阻止

要使用会话阻塞功能,您的应用程序必须使用支持【原子锁】的缓存驱动。目前,支持原子锁的缓存驱动包括 memcacheddynamodbredismongodb(包含在官方的 mongodb/laravel-mongodb 包中)、databasefilearray 驱动。此外,您不能使用 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 方法接受两个可选参数。第一个参数是会话锁定保持的最大秒数,在这个时间到达之前会话锁将被释放。当然,如果请求在此时间之前完成执行,锁会提前释放。

第二个参数是请求在尝试获取会话锁时应该等待的秒数。如果请求无法在给定的秒数内获取到会话锁,则会抛出一个 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 已经提供了文件会话驱动程序,因此您很少需要在此方法中做任何事情。可以将该方法留空。

  • close:与 open 方法类似,close 方法通常也可以忽略。对于大多数驱动程序来说,它不需要做任何操作。

  • read:该方法应返回与给定 $sessionId 关联的会话数据的字符串版本。您不需要对会话数据进行序列化或其它编码操作,因为 Laravel 会为您处理序列化。

  • write:该方法应将与 $sessionId 关联的给定 $data 字符串写入某个持久化存储系统,如 MongoDB 或您选择的其它存储系统。同样,您不需要进行序列化——Laravel 会为您处理。

  • destroy:该方法应从持久化存储中删除与 $sessionId 关联的数据。

  • gc:该方法应销毁所有比给定 $lifetime 更旧的会话数据,$lifetime 是一个 UNIX 时间戳。对于像 Memcached 和 Redis 这样的自过期系统,这个方法可以留空。

注册驱动程序

一旦您的驱动程序实现完成,您就可以将其注册到 Laravel 中。为了将额外的驱动程序添加到 Laravel 的会话后端,您可以使用 Session facade 提供的 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
{
    /**
     * 注册应用服务。
     */
    public function register(): void
    {
        // ...
    }

    /**
     * 启动应用服务。
     */
    public function boot(): void
    {
        Session::extend('mongo', function (Application $app) {
            // 返回一个实现了 SessionHandlerInterface 的实例...
            return new MongoSessionHandler;
        });
    }
}

一旦会话驱动程序注册成功,您可以通过 SESSION_DRIVER 环境变量或在应用的 config/session.php 配置文件中指定 mongo 驱动程序作为应用的会话驱动。