news 2026/9/24 7:09:31

Yii 2 会话与 Cookie 完全指南:对象化封装、自定义存储与安全加固实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Yii 2 会话与 Cookie 完全指南:对象化封装、自定义存储与安全加固实践
  • 后端
  • Web框架

【免费下载链接】yii2

Yii 2: The Fast, Secure and Professional PHP Framework

项目地址:https://gitcode.com/gh_mirrors/yi/yii2
点击查看免费下载

本文基于 Yii 2 官方指南(docs/guide-ja/runtime-sessions-cookies.md,与英文版 docs/guide/runtime-sessions-cookies.md 内容一致)编写。会话(Session)与 Cookie 是让数据跨多次用户请求持久化的基础机制:在原生 PHP 中,你只能通过全局变量$_SESSION$_COOKIE访问它们;而 Yii 2 将两者封装为对象,提供面向对象的访问方式并附带实用的功能增强。读完本文,你将掌握:如何通过Yii::$app->session组件开关会话、读写会话数据与闪存消息;如何把会话存储切换到数据库、缓存、Redis 或 MongoDB;如何以对象方式读取与发送 Cookie;以及如何配置 Cookie 验证、httpOnly/secure/sameSite等安全属性,并正确设置与 Session 安全相关的php.ini参数。

会话(Session):Yii 的对象化封装

与请求(runtime-requests)和响应(runtime-responses)一样,你可以通过session应用组件访问会话。默认情况下,该组件是yii\web\Session的实例,在代码中通过Yii::$app->session获取。

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

打开与关闭会话

会话的打开与关闭通过以下方法完成:

$session = Yii::$app->session; // 检查会话是否已打开 if ($session->isActive) ... // 打开会话 $session->open(); // 关闭会话 $session->close(); // 销毁会话中注册的所有数据 $session->destroy();

你可以多次调用open()close()而不会产生错误,因为内部实现会首先检查会话是否已经打开。从源码看,open()的开头即是if ($this->getIsActive()) { return; },而getIsActive()是对 PHPsession_status() === PHP_SESSION_ACTIVE的封装(见 framework/web/Session.php),因此重复调用是幂等的。

destroy()的内部实现则值得注意(见 framework/web/Session.php):它会先记录当前会话 ID、关闭会话,再重新打开并执行session_unset()session_destroy(),最后恢复会话 ID。该操作仅在会话处于活动状态时生效,因此调用前需确保已open()

访问会话数据

访问会话中保存的数据,有以下等价写法:

$session = Yii::$app->session; // 获取会话变量。以下三种用法等价 $language = $session->get('language'); $language = $session['language']; $language = isset($_SESSION['language']) ? $_SESSION['language'] : null; // 设置会话变量。以下三种用法等价 $session->set('language', 'en-US'); $session['language'] = 'en-US'; $_SESSION['language'] = 'en-US'; // 删除会话变量。以下三种用法等价 $session->remove('language'); unset($session['language']); unset($_SESSION['language']); // 检查会话变量是否存在。以下两种用法等价 if ($session->has('language')) ... if (isset($session['language'])) ... // 遍历所有会话变量。以下两种用法等价 foreach ($session as $name => $value) ... foreach ($_SESSION as $name => $value) ...

Info:通过session组件访问会话数据时,如果会话尚未打开,组件会自动打开会话。这与直接通过$_SESSION访问不同——后者需要你显式调用session_start()

这一“自动打开”行为在源码中有清晰体现:Session::get()set()remove()has()等方法的第一行都是$this->open();(见 framework/web/Session.php),并且Session类实现了\IteratorAggregate\ArrayAccess\Countable接口(见 framework/web/Session.php),所以它能像数组一样被isset()foreachcount()使用。

数组型会话数据的限制与绕行方案

当会话数据是数组时,session组件存在一个限制:无法直接修改数组中的某个元素。例如:

$session = Yii::$app->session; // 下面的代码不会生效 $session['captcha']['number'] = 5; $session['captcha']['lifetime'] = 3600; // 下面的代码生效 $session['captcha'] = [ 'number' => 5, 'lifetime' => 3600, ]; // 下面的代码同样生效(读取不限于顶层) echo $session['captcha']['lifetime'];

原因在于ArrayAccess::offsetSet()只能接收整体值(见 framework/web/Session.php),它无法感知嵌套数组元素的写操作。要解决这个问题,可以使用以下任意一种绕行方案:

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

为了兼顾性能与代码可读性,官方推荐方案四:与其把整个数组作为一个会话变量保存,不如把数组的每个元素分别保存为独立的会话变量,并让它们共享同一个键前缀。

自定义会话存储

默认的yii\web\Session类将会话数据以文件形式保存在服务器上。Yii 2 还提供了下列实现不同存储介质的会话类:

会话类存储介质
yii\web\DbSession使用数据库表保存会话数据
yii\web\CacheSession借助已配置的缓存组件使用缓存保存会话数据
yii\redis\Session使用 Redis 作为存储介质(由官方 redis 扩展提供)
yii\mongodb\Session将会话数据保存在 MongoDB 中(由官方 mongodb 扩展提供)

所有这些会话类都支持同一套 API 方法。因此,你可以在不修改任何使用会话的应用代码的前提下,直接切换会话存储类。

Note:使用自定义会话存储时,如果还想通过$_SESSION访问会话数据,必须确保会话已经由Session::open()启动。这是因为自定义会话存储的处理器(handler)是在open()方法内部注册的。从源码看,registerSessionHandler()会在open()中被调用(见 framework/web/Session.php),当getUseCustomStorage()返回true时,它会创建并注册一个SessionHandler实例(见 framework/web/Session.php)。

Note:使用自定义会话存储时,可能需要对会话垃圾回收器(garbage collector)进行显式配置。某些 PHP 安装(例如 Debian)将垃圾回收概率设为 0,并依靠 cron 作业离线清理会话文件。这一流程对自定义存储并不适用,因此你需要将yii\web\Session::$GCProbability配置为非零值。

关于GCProbability,源码的实现是把百分比换算成 PHP 的session.gc_probability/session.gc_divisor两个 ini 值(见 framework/web/Session.php),取值范围为 0~100(百分比)。

使用数据库表保存会话:DbSession

以下示例展示了如何在应用配置中把session组件配置为yii\web\DbSession,从而使用数据库表作为会话存储:

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

你还需要创建一张如下结构的数据库表来保存会话数据:

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

其中BLOB指你所用 DBMS 的 BLOB 类型。下面是几种常见 DBMS 可用的 BLOB 类型:

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

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

DB 表结构定义与sessionTable属性的文档注释完全对应(见 framework/web/DbSession.php)。源码中还建议在生产环境为expire列建立索引以提升性能,并提示 DbSession 在写会话时使用upsert语句、在回收时执行DELETE ... WHERE [[expire]]<:expire(见 framework/web/DbSession.php)。

作为替代方案,你也可以使用下面的迁移来创建该表:

<?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}}'); } }

使用缓存保存会话:CacheSession

若想使用缓存保存会话数据,可配置yii\web\CacheSession

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

需要注意:从定义上讲缓存存储是易失的(volatile),其中的数据可能被换出而丢失。因此,必须确保CacheSession使用的缓存不是易失的;如果希望以数据库为存储介质,DbSession是更稳妥的选择。源码层面,CacheSession的读写分别通过$this->cache->get()/$this->cache->set()完成,缓存键由[__CLASS__, $id]构成(见 framework/web/CacheSession.php),并且其getUseCustomStorage()恒返回true

闪存数据(Flash Data)

闪存数据是一种特殊的会话数据:它在某个请求中设置后,仅在紧接着的下一次请求中可读,之后会被自动删除。闪存数据最常见的用途是实现只向终端用户显示一次的消息,例如用户成功提交表单后显示的确认信息。

你可以通过session应用组件设置和访问闪存数据。例如:

$session = Yii::$app->session; // 请求 #1 // 设置名为 "postDeleted" 的闪存消息 $session->setFlash('postDeleted', '投稿の削除に成功しました。'); // 请求 #2 // 显示名为 "postDeleted" 的闪存消息 echo $session->getFlash('postDeleted'); // 请求 #3 // 闪存消息已被自动删除,因此 $result 为 false $result = $session->hasFlash('postDeleted');

与普通会话数据一样,任意数据都可以作为闪存数据保存。

调用setFlash()会覆盖同名已有的闪存数据;如果希望向同名既有消息中追加新数据,则应改用addFlash()。例如:

$session = Yii::$app->session; // 请求 #1 // 在 "alerts" 名下追加多条闪存消息 $session->addFlash('alerts', '投稿の削除に成功しました。'); $session->addFlash('alerts', '友達の追加に成功しました。'); $session->addFlash('alerts', 'あなたのレベルが上りました。'); // 请求 #2 // $alerts 是 "alerts" 名下的闪存消息数组 $alerts = $session->getFlash('alerts');

Note:请不要对同名的闪存数据混用setFlash()addFlash()。因为后一个方法会自动把闪存数据转换成数组以便追加,结果调用getFlash()时,你可能会根据这两个方法的调用顺序,时而拿到数组、时而拿到字符串。

从源码看,闪存数据的生命周期由$flashParam(默认键名__flash,见 framework/web/Session.php)下的计数器机制驱动:setFlash()默认以-1标记“读取后删除”,getFlash()读取时将其改为1,而updateFlashCounters()会在下一个请求初始化时把计数大于 0 的闪存数据清除(见 framework/web/Session.php)。此外,setFlash()/addFlash()还支持第三个参数$removeAfterAccess:设为false时,闪存消息在下一个请求结束时无论是否被读取都会被删除。

Tip:要显示闪存消息,可以使用yii\bootstrap\Alert组件(bootstrap Alert 部件),方式如下:

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

Cookie:请求与响应中的对象化表示

Yii 把每一个 Cookie 表示为yii\web\Cookie的对象。yii\web\Requestyii\web\Response都通过名为cookies的属性维护一个 Cookie 集合(yii\web\CookieCollection):前者的集合表示请求中提交上来的 Cookie,后者的集合表示将要发送给用户的 Cookie。

直接操作请求与响应的应用部分是控制器(Controller),因此 Cookie 的读取与发送应在控制器中完成。

读取 Cookie

获取当前请求中的 Cookie:

// 从 "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'])) ...

CookieCollection::has()的实现值得注意:值为空字符串、已标记删除(expire 为过去时间)的 Cookie 都会被视为不存在(见 framework/web/CookieCollection.php)。

发送 Cookie

使用下面的代码向终端用户发送 Cookie:

// 从 "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']);

除了示例中展示的namevalue属性之外,yii\web\Cookie类还定义了其他属性以完整表示所有可用的 Cookie 信息,例如domain(域)、expire(过期时间)、path(路径)、secure(仅 HTTPS)、httpOnly(仅 HTTP)与sameSite(同站策略)。你可以在构造 Cookie 时按需配置这些属性,再将其添加到响应组件的 Cookie 集合中:

$cookies->add(new \yii\web\Cookie([ 'name' => 'language', 'value' => 'zh-CN', 'domain' => 'example.com', 'path' => '/', 'expire' => time() + 86400 * 30, // 30 天后过期 'secure' => true, // 仅在 TLS 连接上发送 'httpOnly' => true, // 禁止客户端脚本访问 ]));

注意,CookieCollection分为只读与可写两种状态:request组件的集合是只读的,对其调用add()/remove()会抛出InvalidCallExceptionresponse组件的集合则可写(见 framework/web/CookieCollection.php)。remove()在默认情况下会把一个过期时间设为 1 的“删除标记”Cookie 加入响应集合,从而让浏览器删除同名 Cookie。

Cookie 验证(防客户端篡改)

如最后两节所示,通过requestresponse组件读写 Cookie 时,你会额外获得Cookie 验证的安全保障,它可以防止 Cookie 在客户端被篡改。其实现方式是:为每个 Cookie 追加一个哈希字符串作为签名,应用通过验证签名即可判断 Cookie 是否在客户端被修改。如果被修改,该 Cookie 将无法通过request组件的 Cookie 集合访问。

Note:Cookie 验证只防止 Cookie 值被修改。即使验证失败,你仍然可以通过$_COOKIE访问该 Cookie。这是为了让第三方库能够以不涉及 Cookie 验证的自身方式操作 Cookie。

Cookie 验证默认启用,你可以通过将yii\web\Request::enableCookieValidation属性设为false来禁用它——但官方强烈建议不要禁用。在源码中,该属性默认值即为true(见 framework/web/Request.php),并且当启用验证时,若未配置验证密钥会直接抛出InvalidConfigException(见 framework/web/Request.php)。

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

使用 Cookie 验证时,必须指定一个用于生成上述哈希字符串的yii\web\Request::cookieValidationKey。你可以在应用配置中配置request组件来完成:

return [ 'components' => [ 'request' => [ 'cookieValidationKey' => 'ここに秘密のキーを書く', ], ], ];

Info:cookieValidationKey对应用安全至关重要。它只能被你信任的人知晓,绝不能提交到版本控制系统中。

从源码看,请求组件解析 Cookie 时会在validateCookies()中调用安全组件Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey)进行签名校验(见 framework/web/Request.php),这再次印证了密钥的重要性——它是 Cookie 签名与验签的唯一凭据。

Cookie 与 Session 的安全设置

yii\web\Cookieyii\web\Session都支持以下安全标志(flag)。

httpOnly

出于安全考虑,yii\web\Cookie::httpOnly以及yii\web\Session::cookieParams中的'httponly'参数的默认值都被设为true。这有助于降低客户端脚本(如 JavaScript)访问受保护 Cookie 的风险(在浏览器支持的前提下)。相关默认值在源码中有明确体现:Cookie::$httpOnly = true(见 framework/web/Cookie.php),而Session$_cookieParams = ['httponly' => true](见 framework/web/Session.php)。

secure

secure标志的目的是防止 Cookie 以明文形式发送。在浏览器支持该标志的情况下,只有当请求通过安全(TLS)连接发送时,Cookie 才会被包含在请求中。配置方式:在发送 Cookie 时设置'secure' => true,或通过Session::setCookieParams()secure参数传给会话 Cookie。

sameSite

从 Yii 2.0.21 开始支持yii\web\Cookie::sameSite设置,且要求 PHP 7.4.0 或更高版本(对Session::cookieParams中的sameSite键,源码要求 PHP 7.3.0 及以上,见 framework/web/Session.php)。sameSite设置的目的是防御 CSRF(跨站请求伪造)攻击:在浏览器支持该设置的情况下,只有符合指定策略('Lax''Strict')的 Cookie 才会被发送。为了安全,在不支持该特性的 PHP 版本上使用sameSite会抛出异常。若想跨 PHP 版本使用该特性,请先进行版本检查,例如:

[ 'sameSite' => PHP_VERSION_ID >= 70300 ? yii\web\Cookie::SAME_SITE_LAX : null, ]

源码中,yii\web\Cookie定义了三个 SameSite 策略常量(见 framework/web/Cookie.php):

  • SAME_SITE_LAX = 'Lax':在跨站上下文中,CSRF 高危请求方法(如 POST、PUT、PATCH)不会携带该 Cookie,但普通链接跳转(GET)会携带;
  • SAME_SITE_STRICT = 'Strict':无论请求方法如何,所有跨站上下文中都不携带该 Cookie,即使是通过普通链接跳转;
  • SAME_SITE_NONE = 'None'(自 2.0.43 起):Cookie 在所有上下文中都会被发送;注意若设置为None,则必须同时将secure设为true,否则浏览器会阻止该 Cookie。

同时,Session::setCookieParams()在 PHP 7.3.0 以下会通过把samesite拼接进path的方式降级兼容(见 framework/web/Session.php)。

Note:目前仍有部分浏览器不支持sameSite设置,因此强烈建议同时采取附加的 CSRF 防护措施。

与会话相关的 php.ini 设置

如 PHP 手册所述,php.ini中包含若干重要的会话安全设置。请务必应用官方推荐配置,尤其是session.use_strict_mode——它在 PHP 安装中的默认状态下并未启用。该设置也可以通过yii\web\Session::useStrictMode属性进行配置:

return [ 'components' => [ 'session' => [ 'useStrictMode' => true, ], ], ];

从源码看,setUseStrictMode()最终通过ini_set('session.use_strict_mode', ...)生效(见 framework/web/Session.php);开启后,Session::open()会在检测到未初始化的会话 ID 时强制调用regenerateID()重新生成会话 ID(见 framework/web/Session.php),从而防范会话固定(session fixation)攻击。这一行为在DbSessionCacheSession中还有更严格的实现:开启严格模式后,openSession()会先检查该 ID 在存储介质中是否真实存在,若不存在则标记为需强制重新生成(见 framework/web/DbSession.php)。

此外,值得一并留意的相关 ini 项包括:session.gc_probability/session.gc_divisor(垃圾回收概率,对应Session::$GCProbability,默认 1440 秒的超时由session.gc_maxlifetime决定,见 framework/web/Session.php)、session.use_cookies/session.use_only_cookies(会话 ID 的传递方式,对应Session::$useCookies,见 framework/web/Session.php)。在多服务器部署等场景中,请结合实际运行环境审慎调整这些参数,并建议将会话存储切换为DbSessionCacheSession等共享存储,以保证会话数据在多个节点之间一致可用。

总结

Yii 2 通过Yii::$app->sessionrequest/response组件的cookies集合,把原生 PHP 的$_SESSION$_COOKIE全面对象化,提供了幂等的开关方法、数组风格的读写语法、自动打开的会话生命周期,以及一次性读取的闪存消息机制。在此基础上:

  • 存储可插拔DbSessionCacheSessionredis\Sessionmongodb\Session共享同一套 API,可在不改业务代码的前提下切换会话存储;
  • Cookie 防篡改:默认启用的 Cookie 验证 + 必须配置的cookieValidationKey,让客户端篡改的 Cookie 无法通过request组件被读取;
  • 安全默认值httpOnly默认开启,sameSite自 2.0.21 起支持,配合secure标志与php.inisession.use_strict_mode,可构建符合现代安全要求的会话与 Cookie 体系。

需要深入研读实现时,可以继续查看 framework/web/Session.php、framework/web/DbSession.php、framework/web/CacheSession.php、framework/web/Cookie.php、framework/web/CookieCollection.php 与 framework/web/Request.php,并结合 docs/guide-ja/runtime-requests.md(请求)、docs/guide-ja/runtime-responses.md(响应)与 docs/guide-ja/security-best-practices.md(安全最佳实践)构建完整的运行时知识体系。

  • 后端
  • Web框架

【免费下载链接】yii2

Yii 2: The Fast, Secure and Professional PHP Framework

项目地址:https://gitcode.com/gh_mirrors/yi/yii2
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 7:06:04

在 CI/CD 流水线中使用 Regal 对 Rego 策略进行代码检查

后端认证鉴权云原生 【免费下载链接】opa Open Policy Agent (OPA) is an open source, general-purpose policy engine. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/op/opa 点击查看 免费下载 Regal 是 Open Policy Agent 生态中专门用于 Rego 策略代码的 linter …

作者头像 李华
网站建设 2026/9/24 7:05:18

Qt工程打包为exe全流程:从windeployqt到安装包制作

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 6:58:06

代理IP团队化管理与选型实战:从API批量配IP到子账户权限

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 6:50:35

Qwen3.8-Flash 限时免费:9 月 30 日前在 Qoder 零 Credits 畅用

Qwen3.8-Flash 限时免费&#xff1a;9 月 30 日前在 Qoder 零 Credits 畅用 9 月 18 日&#xff0c;阿里 Agentic 编码平台 Qoder 官方宣布&#xff1a;Qwen3.8-Flash 模型限时免费开放&#xff0c;活动期为 2026 年 9 月 18 日 10:00 至 9 月 30 日 23:59:59。活动期间该模型…

作者头像 李华
网站建设 2026/9/24 6:46:05

西门子车辆PLM一期方案拆解:NX集成与BOM管理落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华