Yii2 Sessions 与 Cookies 权威实战指南:会话生命周期、Flash 数据与 Cookie 验证机制

原创2026-09-23 13:23:01479 阅读
文章标签:后端Web框架

Yii2 Sessions 与 Cookies 权威实战指南:会话生命周期、Flash 数据与 Cookie 验证机制

本篇技术指南基于 Yii2 框架(yii\web\Session、yii\web\Cookie 及其配套组件)系统讲解会话(Session)与 Cookie 的完整使用方案。你将掌握:如何在 Yii2 中以面向对象方式开启、读写、销毁会话;如何将会话存储切换到数据库、缓存、Redis 等介质;如何利用 Flash 数据实现"仅显示一次"的用户提示;以及如何配置 Cookie 验证(cookieValidationKey)防止客户端篡改。文中所有示例均可直接复制运行,并附有框架源码级实现佐证。

Sessions 在 Yii2 中的定位

在纯 PHP 中,跨请求的数据保持依赖全局变量 $_SESSION 与 $_COOKIE。Yii2 将这两者封装为对象并增加了若干能力(自动开启、数组式访问、可插拔存储、Flash 数据、Cookie 签名验证等),让开发者可以通过面向对象的方式访问它们。

和请求、响应类似,默认情况下通过名为 session 的应用组件(即 yii\web\Session 的实例)来访问会话:

$session = Yii::$app->session;

yii\web\Session(源码见 framework/web/Session.php)实现了 \IteratorAggregate、\ArrayAccess、\Countable 三个接口,因此它可以像数组一样被读取、写入、遍历和计数。此外它继承自 yii\base\Component,可通过 init() 在应用初始化时注册 close() 到 register_shutdown_function,确保请求结束时会话数据被妥善写回(见 Session::init())。

开启和关闭 Sessions

使用以下代码控制会话的开启与关闭:

$session = Yii::$app->session;

// 检查 session 是否开启
if ($session->isActive) ...

// 开启 session
$session->open();

// 关闭 session(写入并保存数据)
$session->close();

// 销毁 session 中所有已注册的数据
$session->destroy();

多次调用 open() 和 close() 并不会产生错误:Session::open()(framework/web/Session.php)内部先通过 getIsActive()(即 session_status() === PHP_SESSION_ACTIVE)判断是否已开启,已开启则直接返回;close() 则仅在会话激活时调用 session_write_close()。

从源码可以看到 open() 的完整流程:

  1. 调用 registerSessionHandler() 注册自定义会话处理器(如果配置了 handler 属性或使用自定义存储);
  2. 若配置使用 Cookie 传递会话 ID,则应用会话 Cookie 参数(默认 httponly => true);
  3. 执行 session_start()(DEBUG 模式下不抑制错误输出);
  4. 若启用严格模式且检测到需要强制重新生成 ID,则调用 regenerateID();
  5. 会话成功激活后更新 Flash 计数器。

destroy() 的实现值得注意(framework/web/Session.php):它会先记录当前 session_id(),依次执行 close()、setId($sessionId)、open()、session_unset()、session_destroy(),最后恢复 ID,从而确保销毁动作完整且不留残余数据。另外,Session::init() 还会调用 register_shutdown_function([$this, 'close']),即使你忘记手动关闭会话,请求结束时数据也会被自动写回。

访问 Session 数据

Yii2 的 session 组件与 $_SESSION 提供了等价的读写能力,且支持数组下标语法:

$session = Yii::$app->session;

// 获取 session 中的变量值,以下用法是相同的:
$language = $session->get('language');
$language = $session['language'];
$language = isset($_SESSION['language']) ? $_SESSION['language'] : null;

// 设置一个 session 变量,以下用法是相同的:
$session->set('language', 'en-US');
$session['language'] = 'en-US';
$_SESSION['language'] = 'en-US';

// 删除一个 session 变量,以下用法是相同的:
$session->remove('language');
unset($session['language']);
unset($_SESSION['language']);

// 检查 session 变量是否已存在,以下用法是相同的:
if ($session->has('language')) ...
if (isset($session['language'])) ...
if (isset($_SESSION['language'])) ...

// 遍历所有 session 变量,以下用法是相同的:
foreach ($session as $name => $value) ...
foreach ($_SESSION as $name => $value) ...

Info:通过 session 组件访问会话数据时,如果会话尚未开启会被自动开启(offsetExists()、offsetGet() 等方法内部都会先调用 open());而直接使用 $_SESSION 则要求先执行 session_start()。

数组型数据的写入陷阱与四种变通方案

当会话数据是数组时,session 组件不支持直接修改数组中的某个单元项(因为数组下标访问会触发 PHP 的"写时复制"机制,嵌套数组的单元修改无法直接写回会话存储):

$session = Yii::$app->session;

// 如下代码不会生效
$session['captcha']['number'] = 5;
$session['captcha']['lifetime'] = 3600;

// 如下代码会生效(整体覆盖):
$session['captcha'] = [
    'number' => 5,
    'lifetime' => 3600,
];

// 如下代码也会生效(整体读取):
echo $session['captcha']['lifetime'];

可以使用以下任意一种变通方法:

$session = Yii::$app->session;

// 方案一:直接使用 $_SESSION(确保 Yii::$app->session->open() 已经调用)
$_SESSION['captcha']['number'] = 5;
$_SESSION['captcha']['lifetime'] = 3600;

// 方案二:先取出数组、修改后再整体写回
$captcha = $session['captcha'];
$captcha['number'] = 5;
$captcha['lifetime'] = 3600;
$session['captcha'] = $captcha;

// 方案三:使用 ArrayObject 数组对象代替数组
$session['captcha'] = new \ArrayObject;
...
$session['captcha']['number'] = 5;
$session['captcha']['lifetime'] = 3600;

// 方案四:使用带通用前缀的键来存储数组的每个单元
$session['captcha.number'] = 5;
$session['captcha.lifetime'] = 3600;

为获得更好的性能和可读性,推荐最后一种方案:不要把整个数组存成一个 session 变量,而是将每个数组项变成具有相同键前缀的独立 session 变量。

自定义 Session 存储

yii\web\Session 默认将会话数据以文件形式保存在服务器上(由 php.ini 的 session.save_path 决定)。Yii2 提供了以下会话类以实现不同的存储介质:

会话类 存储介质 说明
yii\web\Session 文件 默认实现,无需额外配置
yii\web\DbSession 数据库表 数据持久、可靠,适合生产环境,源码见 framework/web/DbSession.php
yii\web\CacheSession 缓存组件 使用配置中的缓存组件,源码见 framework/web/CacheSession.php
yii\redis\Session Redis 需安装 yiisoft/yii2-redis 扩展
yii\mongodb\Session MongoDB 需安装 yiisoft/yii2-mongodb 扩展

所有这些会话类都支持相同的 API 方法集(get/set/remove/has/open/close/destroy 等),因此切换到不同的存储介质不需要修改任何业务代码,只需调整应用配置。

从源码看,自定义存储的实现机制是:子类重写 getUseCustomStorage() 返回 true,并实现 openSession()、closeSession()、readSession()、writeSession()、destroySession()、gcSession() 六个处理器方法(见 framework/web/Session.php 的说明)。open() 时 registerSessionHandler() 会通过 Yii::createObject 构造 yii\web\SessionHandler,并将子类实例作为 SessionHandlerInterface 注册给 PHP 的 session_set_save_handler()。

Note:如果通过 $_SESSION 访问使用自定义存储介质的会话,需要确保会话已经通过 open() 开启,因为自定义存储处理器正是在 open() 中被注册的。

Note:使用自定义会话存储时,可能需要显式配置会话垃圾收集器(GC)。部分 PHP 发行版(如 Debian)将垃圾收集概率设为 0,并依赖计划任务离线清理会话文件;该过程不适用于自定义存储,因此需要将 yii\web\Session::$GCProbability 配置为非零值。

使用 DbSession 将会话存入数据库

以下配置将 session 组件切换到数据库存储:

return [
    'components' => [
        'session' => [
            'class' => 'yii\web\DbSession',
            // 'db' => 'mydb',  // 数据库连接的应用组件 ID,默认为 'db'
            // 'sessionTable' => 'my_session', // session 数据表名,默认为 'session'
        ],
    ],
];

对应的数据库表结构(BLOB 需替换为你所用数据库管理系统的 BLOB 类型):

CREATE TABLE session
(
    id CHAR(40) NOT NULL PRIMARY KEY,
    expire INTEGER,
    data BLOB
)

常用数据库管理系统的 BLOB 类型:

  • MySQL:LONGBLOB
  • PostgreSQL:BYTEA
  • MSSQL:BLOB

Note:根据 php.ini 中 session.hash_function 的设置,你可能需要调整 id 列的长度。例如若 session.hash_function=sha256,应使用长度为 64 的 char 类型而不是 40。

也可以使用 Yii2 迁移来创建该表:

<?php

use yii\db\Migration;

class m170529_050554_create_table_session extends Migration
{
    public function up()
    {
        $this->createTable('{{%session}}', [
            'id' => $this->char(64)->notNull(),
            'expire' => $this->integer(),
            'data' => $this->binary()
        ]);
        $this->addPrimaryKey('pk-id', '{{%session}}', 'id');
    }

    public function down()
    {
        $this->dropTable('{{%session}}');
    }
}

结合 framework/web/DbSession.php 的源码可以进一步了解其内部行为:

  • $db 默认指向 'db' 应用组件,$sessionTable 默认值为 '{{%session}}'(即带表前缀的 session 表),init() 中通过 Instance::ensure() 解析数据库连接;
  • 读取时 readSession() 通过查询构造器按 expire > 当前时间 AND id = 会话ID 过滤过期数据;
  • 写入时 writeSession() 使用 upsert() 执行插入或更新,并强制将 expire 设为 time() + getTimeout();
  • 销毁时 destroySession() 删除对应行;垃圾回收 gcSession() 删除所有 expire < 当前时间 的记录;
  • DbSession 还支持 writeCallback 在写会话时携带额外字段(继承自 MultiFieldSession);
  • 生产环境下,建议为 expire 列建立数据库索引以提升性能;
  • 启用了 useStrictMode 时,openSession() 会校验会话 ID 是否真实存在于数据表中,不存在则标记强制重新生成 ID,可有效抵御会话固定(Session Fixation)攻击。

使用 CacheSession 将会话存入缓存

return [
    'components' => [
        'session' => [
            'class' => 'yii\web\CacheSession',
            // 'cache' => 'mycache', // 缓存组件 ID,默认为 'cache'
        ],
    ],
];

CacheSession 使用任意 yii\caching\CacheInterface 缓存组件作为存储介质(源码见 framework/web/CacheSession.php)。需要注意的是,缓存本质上是易失的,数据可能因容量淘汰或过期而被清除,因此务必确保所用的缓存组件不是易失性的;如果希望数据可靠持久,DbSession 是更合适的选择。

Flash 数据

Flash 数据是一种特殊的会话数据:它在某个请求中设置后,只在下一个请求中有效,随后自动删除。它常用于实现"只向终端用户显示一次"的信息,例如表单提交成功后的确认提示。

$session = Yii::$app->session;

// 请求 #1
// 设置一个名为 "postDeleted" 的 flash 信息
$session->setFlash('postDeleted', 'You have successfully deleted your post.');

// 请求 #2
// 显示名为 "postDeleted" 的 flash 信息
echo $session->getFlash('postDeleted');

// 请求 #3
// $result 为 false,因为 flash 信息已被自动删除
$result = $session->hasFlash('postDeleted');

与普通会话数据一样,任意类型的数据都可以作为 flash 数据存储。

setFlash() 会覆盖同名的已有数据;若希望向同名的 flash 中追加数据,应使用 addFlash():

$session = Yii::$app->session;

// 请求 #1
// 向名称为 "alerts" 的 flash 信息中追加数据
$session->addFlash('alerts', 'You have successfully deleted your post.');
$session->addFlash('alerts', 'You have successfully added a new friend.');
$session->addFlash('alerts', 'You are promoted.');

// 请求 #2
// $alerts 为名为 'alerts' 的 flash 信息,此时为数组格式
$alerts = $session->getFlash('alerts');

Note:不要在同一个 flash 键上混用 setFlash() 和 addFlash(),因为 addFlash() 会自动把已存在的 flash 值转换为数组以便追加;当你调用 getFlash() 时,获取到的是数组还是字符串,将取决于你调用这两个方法的先后顺序。

Flash 数据的底层实现机制

从 framework/web/Session.php 的源码可以还原 Flash 的完整生命周期:

  • 会话内部使用名为 __flash 的会话变量($flashParam,可通过属性覆盖)维护一个计数器数组;
  • setFlash($key, $value, $removeAfterAccess = true):将计数器记为 -1(表示"读取后删除")或 0(表示"无论是否读取,下次请求后都删除"),并把值写入 $_SESSION[$key];
  • getFlash($key, $defaultValue = null, $delete = false):读取值时若计数器为负数(-1),会先将其标记为 1("下次请求删除"),这样本次请求仍可读到,下一次请求的 updateFlashCounters() 会将其连同数据一并清理;
  • addFlash($key, $value, $removeAfterAccess = true):将新值追加到已有数组末尾,若原值不是数组则先包装为数组;
  • updateFlashCounters() 在每次会话开启(open())时被调用,负责执行上一轮请求标记的删除动作——这正是"仅在下一次请求中有效"这一语义的落实之处;
  • 计数器不是数组时会被安全地清除(unset),以规避异常数据导致的报错。

也就是说,Flash 的"一次有效"并不是魔法,而是依赖 open() 时对 __flash 计数器的递增与过期清理实现的,理解这一点有助于排查"Flash 消息提前消失"或"迟迟不消失"的问题。

用 Bootstrap Alert 渲染 Flash 消息

Yii2 官方推荐通过 yii\bootstrap\Alert 小部件渲染 Flash 消息(需安装 yiisoft/yii2-bootstrap 扩展):

echo Alert::widget([
    'options' => ['class' => 'alert-info'],
    'body' => Yii::$app->session->getFlash('postDeleted'),
]);

Cookies

Yii2 使用 yii\web\Cookie 对象代表每一个 Cookie(源码见 framework/web/Cookie.php)。yii\web\Request 与 yii\web\Response 分别通过名为 cookies 的属性维护一个 yii\web\CookieCollection 集合(源码见 framework/web/CookieCollection.php):前者表示请求提交上来的 Cookie,后者表示将要发送给用户的 Cookie。

由于控制器是直接处理请求与响应的部分,应当在控制器中读取和发送 Cookie(在控制器层面处理 Cookie 是安全的)。

读取 Cookies

// 从 "request" 组件中获取 cookie 集合(yii\web\CookieCollection)
$cookies = Yii::$app->request->cookies;

// 获取名为 "language" cookie 的值,如果不存在,返回默认值 "en"
$language = $cookies->getValue('language', 'en');

// 另一种方式获取名为 "language" cookie 的值
if (($cookie = $cookies->get('language')) !== null) {
    $language = $cookie->value;
}

// 可将 $cookies 当作数组使用
if (isset($cookies['language'])) {
    $language = $cookies['language']->value;
}

// 判断是否存在名为 "language" 的 cookie
if ($cookies->has('language')) ...
if (isset($cookies['language'])) ...

源码层面的细节:Request::getCookies()(framework/web/Request.php)基于 loadCookies() 的返回值构造 CookieCollection 并将其标记为 readOnly => true,因此请求端的 cookie 集合是只读的,任何 add()/remove() 操作都会抛出 InvalidCallException;CookieCollection::has() 会同时判断 cookie 是否存在、值是否为空字符串以及是否已过期。Cookie 对象还实现了 __toString(),可直接将 cookie 转为字符串。

发送 Cookies

// 从 "response" 组件中获取 cookie 集合(yii\web\CookieCollection)
$cookies = Yii::$app->response->cookies;

// 在要发送的响应中添加一个新的 cookie
$cookies->add(new \yii\web\Cookie([
    'name' => 'language',
    'value' => 'zh-CN',
]));

// 删除一个 cookie
$cookies->remove('language');
// 等同于以下删除代码
unset($cookies['language']);

CookieCollection::remove()(framework/web/CookieCollection.php)默认会生成一个 expire => 1 的过期 Cookie 加入响应集合,从而通知浏览器删除该 Cookie。

除了 name 和 value,yii\web\Cookie 还定义了以下常用属性,配置后随 Cookie 一起发送:

属性 默认值 说明
name (必填) Cookie 名称
value '' Cookie 值
domain '' Cookie 生效的域名
expire 0 过期时间戳(服务器时间);0 或 null 表示"关闭浏览器即失效",也可传 \DateTimeInterface 或可被 strtotime() 解析的字符串
path '/' Cookie 在服务器端生效的路径
secure false 是否仅通过 HTTPS 安全连接发送
httpOnly true 是否仅允许 HTTP 协议访问(禁止 JavaScript 读取,降低 XSS 窃取身份的风险)
sameSite Lax SameSite 策略,可选 Lax、Strict、None(对应源码中的 SAME_SITE_LAX、SAME_SITE_STRICT、SAME_SITE_NONE 常量;设为 None 时必须同时启用 secure,否则浏览器会拦截该 Cookie)

Note:为安全起见,httpOnly 默认即为 true,这可以减少客户端脚本访问受保护 Cookie 的风险。

Cookie 验证

通过 request 和 response 组件读取与发送 Cookie 时,框架默认启用了 Cookie 验证:它为每个 Cookie 签发一个哈希字符串,服务端据此判断 Cookie 是否在客户端被篡改。如果验证失败,该 Cookie 将无法通过 request 组件的 cookie 集合访问到。

Cookie 验证默认启用。可以设置 yii\web\Request::enableCookieValidation 为 false 禁用,但强烈建议保持启用。

Note:Cookie 验证只保护 Cookie 值不被修改。如果一个 Cookie 验证失败,仍然可以通过 $_COOKIE 直接访问它(这是第三方库对未通过验证 Cookie 的操作方式)。

Note:直接通过 $_COOKIE 读取、通过 setcookie() 发送的 Cookie 不会被验证。

启用 Cookie 验证时,必须配置 yii\web\Request::cookieValidationKey(用于生成哈希值),在应用配置中设置 request 组件:

return [
    'components' => [
        'request' => [
            'cookieValidationKey' => 'fill in a secret key here',
        ],
    ],
];

Info:cookieValidationKey 对应用安全至关重要,应只被你信任的人知晓,不要把它放入版本控制(建议写入环境变量或本地非提交配置)。

验证的底层实现

从源码可以确认验证的双向流程:

  • 发送端(Response::sendCookies(),framework/web/Response.php):若启用了 Cookie 验证,先检查 cookieValidationKey 是否为空(为空直接抛出 InvalidConfigException),然后对 serialize([$cookie->name, $cookie->value]) 调用 Yii::$app->getSecurity()->hashData() 生成带签名的值再发送;expire == 1 的删除标记 Cookie 不会被签名。
  • 接收端(Request::loadCookies(),framework/web/Request.php):同样要求 cookieValidationKey 非空,对 $_COOKIE 中的每个值调用 getSecurity()->validateData() 校验签名,校验失败的数据会被跳过,从而不会出现在 Yii::$app->request->cookies 中。

因此,Cookie 验证的本质是对称密钥签名:只要 cookieValidationKey 保密,客户端就无法在不知道密钥的情况下伪造或篡改 Cookie 值而不被发现。若 Key 泄露,攻击者即可伪造合法签名的 Cookie,请务必妥善保管并及时轮换。

小结

在 Yii2 中,会话与 Cookie 的处理遵循统一的对象化封装:

  • 会话:通过 Yii::$app->session 以对象/数组方式读写;open()、close()、destroy() 控制生命周期;通过切换 DbSession、CacheSession、Redis/MongoDB 会话类即可更换存储介质而无需改动业务代码;Flash 数据基于 __flash 计数器实现"仅一次有效"语义。
  • Cookie:Yii::$app->request->cookies 只读集合负责读取,Yii::$app->response->cookies 负责发送;Cookie 对象提供 domain、expire、secure、httpOnly、sameSite 等属性精细控制。
  • 安全:Cookie 验证默认开启,务必配置高强度的 cookieValidationKey 并保密;生产环境建议为 DbSession 的 expire 列建索引并合理配置 GC 概率。

相关主题可继续阅读:请求组件、响应组件、应用组件、缓存数据。

登录后查看全文
yii2