Yii2 Sessions 与 Cookies 权威实战指南:会话生命周期、Flash 数据与 Cookie 验证机制
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() 的完整流程:
- 调用
registerSessionHandler()注册自定义会话处理器(如果配置了handler属性或使用自定义存储); - 若配置使用 Cookie 传递会话 ID,则应用会话 Cookie 参数(默认
httponly => true); - 执行
session_start()(DEBUG 模式下不抑制错误输出); - 若启用严格模式且检测到需要强制重新生成 ID,则调用
regenerateID(); - 会话成功激活后更新 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 概率。