跳转至内容

HTTP Session

简介

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

Laravel 附带了多种 Session 后端,通过富有表现力且统一的 API 进行访问。它支持诸如 MemcachedRedis 和数据库等流行的后端。

配置

应用程序的 Session 配置文件存储在 config/session.php 中。请务必查看该文件中可用的选项。默认情况下,Laravel 配置为使用 database Session 驱动。

Session 的 driver 配置项定义了每个请求的 Session 数据存储在哪里。Laravel 包含多种驱动:

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

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

驱动前提条件

数据库

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

1php artisan make:session-table
2 
3php artisan migrate

Redis

在使用 Redis Session 之前,需要通过 PECL 安装 PhpRedis PHP 扩展,或者通过 Composer 安装 predis/predis 包 (~1.0)。有关配置 Redis 的更多信息,请查阅 Laravel 的 Redis 文档

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

与 Session 交互

获取数据

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

1<?php
2 
3namespace App\Http\Controllers;
4 
5use Illuminate\Http\Request;
6use Illuminate\View\View;
7 
8class UserController extends Controller
9{
10 /**
11 * Show the profile for the given user.
12 */
13 public function show(Request $request, string $id): View
14 {
15 $value = $request->session()->get('key');
16 
17 // ...
18 
19 $user = $this->users->find($id);
20 
21 return view('user.profile', ['user' => $user]);
22 }
23}

当从 Session 中检索项时,可以将默认值作为 get 方法的第二个参数传递。如果指定的键在 Session 中不存在,将返回此默认值。如果你传递一个闭包作为 get 方法的默认值,且请求的键不存在,该闭包将被执行并返回其结果。

1$value = $request->session()->get('key', 'default');
2 
3$value = $request->session()->get('key', function () {
4 return 'default';
5});

全局 Session 辅助函数

你还可以使用全局 session PHP 函数来检索和存储 Session 数据。当使用单个字符串参数调用 session 辅助函数时,它将返回该 Session 键的值。当使用键值对数组调用辅助函数时,这些值将被存储在 Session 中。

1Route::get('/home', function () {
2 // Retrieve a piece of data from the session...
3 $value = session('key');
4 
5 // Specifying a default value...
6 $value = session('key', 'default');
7 
8 // Store a piece of data in the session...
9 session(['key' => 'value']);
10});

使用 HTTP 请求实例访问 Session 与使用全局 session 辅助函数之间几乎没有实际区别。这两种方法都可以通过在所有测试用例中可用的 assertSessionHas 方法进行 测试

获取所有 Session 数据

如果你想获取 Session 中的所有数据,可以使用 all 方法。

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

获取部分 Session 数据

onlyexcept 方法可用于获取 Session 数据的一个子集。

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

判断 Session 中是否存在某项

要判断某项是否存在于 Session 中,可以使用 has 方法。如果该项存在且不为 nullhas 方法返回 true

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

即使某项的值为 null,若要判断其是否存在于 Session 中,可以使用 exists 方法。

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

若要判断某项不存在于 Session 中,可以使用 missing 方法。如果该项不存在,missing 方法返回 true

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

存储数据

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

1// Via a request instance...
2$request->session()->put('key', 'value');
3 
4// Via the global "session" helper...
5session(['key' => 'value']);

向数组 Session 值中追加数据

push 方法可用于将一个新值推入数组形式的 Session 值中。例如,如果 user.teams 键包含一个团队名称数组,你可以像这样将新值推入该数组。

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

检索并删除某项

pull 方法可以在单条语句中检索并删除 Session 中的某项。

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

递增和递减 Session 值

如果你的 Session 数据包含一个需要递增或递减的整数,可以使用 incrementdecrement 方法。

1$request->session()->increment('count');
2 
3$request->session()->increment('count', $incrementBy = 2);
4 
5$request->session()->decrement('count');
6 
7$request->session()->decrement('count', $decrementBy = 2);

闪存数据 (Flash Data)

有时你可能希望在下一次请求中临时存储一些数据,可以使用 flash 方法。使用此方法存储的数据在当前请求和随后的 HTTP 请求中可用。随后的 HTTP 请求结束后,闪存数据将被删除。闪存数据主要用于短期状态消息。

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

如果你需要将闪存数据保留几个请求,可以使用 reflash 方法,它会将所有闪存数据保留额外的请求。如果你只需要保留特定的闪存数据,可以使用 keep 方法。

1$request->session()->reflash();
2 
3$request->session()->keep(['username', 'email']);

若要仅在当前请求中持久化闪存数据,可以使用 now 方法。

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

删除数据

forget 方法将从 Session 中移除一条数据。如果你想移除 Session 中的所有数据,可以使用 flush 方法。

1// Forget a single key...
2$request->session()->forget('name');
3 
4// Forget multiple keys...
5$request->session()->forget(['name', 'status']);
6 
7$request->session()->flush();

重新生成 Session ID

重新生成 Session ID 通常是为了防止恶意用户利用应用程序的 会话固定 (Session fixation) 攻击。

如果你使用的是 Laravel 入门套件Laravel Fortify,Laravel 会在认证期间自动重新生成 Session ID;但如果你需要手动重新生成 Session ID,可以使用 regenerate 方法。

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

如果你需要在单条语句中重新生成 Session ID 并清除 Session 中的所有数据,可以使用 invalidate 方法。

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

Session 缓存

Laravel 的 Session 缓存提供了一种便捷的方式来缓存作用于单个用户 Session 的数据。与全局应用程序缓存不同,Session 缓存数据会自动隔离,并在 Session 过期或销毁时被清理。Session 缓存支持所有熟悉的 Laravel 缓存方法,如 getputrememberforget 等,但仅限于当前 Session。

Session 缓存非常适合存储临时的、特定于用户的数据,这些数据需要在同一 Session 的多个请求之间保留,但无需永久存储。这包括表单数据、临时计算结果、API 响应或任何其他应绑定到特定用户 Session 的瞬态数据。

你可以通过 Session 上的 cache 方法访问 Session 缓存。

1$discount = $request->session()->cache()->get('discount');
2 
3$request->session()->cache()->put(
4 'discount', 10, now()->plus(minutes: 5)
5);

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

Session 阻塞

要使用 Session 阻塞,你的应用程序必须使用支持 原子锁 (Atomic locks) 的缓存驱动。目前,这些缓存驱动包括 memcacheddynamodbredismongodb(包含在官方的 mongodb/laravel-mongodb 包中)、databasefilearray 驱动。此外,不能使用 cookie Session 驱动。

默认情况下,Laravel 允许使用同一 Session 的请求并发执行。例如,如果你使用 JavaScript HTTP 库向应用程序发送两个 HTTP 请求,它们会同时执行。对于许多应用程序来说,这不是问题;但在少数应用程序中,如果并发请求同时写入 Session,可能会导致 Session 数据丢失。

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

1Route::post('/profile', function () {
2 // ...
3})->block($lockSeconds = 10, $waitSeconds = 10);
4 
5Route::post('/order', function () {
6 // ...
7})->block($lockSeconds = 10, $waitSeconds = 10);

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

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

如果未传递这些参数,锁最多持有 10 秒,请求在尝试获取锁时最多等待 10 秒。

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

添加自定义 Session 驱动

实现驱动

如果现有的 Session 驱动均无法满足你的应用程序需求,Laravel 允许编写自定义的 Session 处理程序。自定义 Session 驱动应实现 PHP 内置的 SessionHandlerInterface。该接口仅包含几个简单的方法。以下是一个 MongoDB 实现的存根示例:

1<?php
2 
3namespace App\Extensions;
4 
5class MongoSessionHandler implements \SessionHandlerInterface
6{
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 驱动,你几乎不需要在此方法中填入任何内容。你可以简单地将其留空。
  • close 方法与 open 方法一样,通常也可以忽略。对于大多数驱动来说,它是不必要的。
  • read 方法应返回与给定的 $sessionId 关联的 Session 数据字符串版本。在驱动中检索或存储 Session 数据时,无需进行任何序列化或编码,因为 Laravel 会为你处理序列化。
  • write 方法应将与 $sessionId 关联的 $data 字符串写入持久化存储系统(如 MongoDB 或其他你选择的系统)。同样,你不应执行任何序列化,因为 Laravel 已经为你处理好了。
  • destroy 方法应从持久化存储中移除与 $sessionId 关联的数据。
  • gc 方法应销毁所有早于给定 $lifetime(Unix 时间戳)的 Session 数据。对于 Memcached 和 Redis 等支持自过期的系统,此方法可以留空。

注册驱动

驱动实现完成后,即可将其注册到 Laravel。要向 Laravel 的 Session 后端添加额外驱动,可以使用 Session 门面 (Facade) 提供的 extend 方法。你应该在 服务提供者boot 方法中调用 extend 方法。你可以将其放在现有的 App\Providers\AppServiceProvider 中,或创建一个全新的提供者。

1<?php
2 
3namespace App\Providers;
4 
5use App\Extensions\MongoSessionHandler;
6use Illuminate\Contracts\Foundation\Application;
7use Illuminate\Support\Facades\Session;
8use Illuminate\Support\ServiceProvider;
9 
10class SessionServiceProvider extends ServiceProvider
11{
12 /**
13 * Register any application services.
14 */
15 public function register(): void
16 {
17 // ...
18 }
19 
20 /**
21 * Bootstrap any application services.
22 */
23 public function boot(): void
24 {
25 Session::extend('mongo', function (Application $app) {
26 // Return an implementation of SessionHandlerInterface...
27 return new MongoSessionHandler;
28 });
29 }
30}

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