- 后端
- Web框架
【免费下载链接】yii2
Yii 2: The Fast, Secure and Professional PHP Framework
本文基于 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()、foreach、count()使用。
数组型会话数据的限制与绕行方案
当会话数据是数组时,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.ini中session.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\Request和yii\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']);除了示例中展示的name和value属性之外,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()会抛出InvalidCallException;response组件的集合则可写(见 framework/web/CookieCollection.php)。remove()在默认情况下会把一个过期时间设为 1 的“删除标记”Cookie 加入响应集合,从而让浏览器删除同名 Cookie。
Cookie 验证(防客户端篡改)
如最后两节所示,通过request和response组件读写 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\Cookie与yii\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)攻击。这一行为在DbSession与CacheSession中还有更严格的实现:开启严格模式后,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)。在多服务器部署等场景中,请结合实际运行环境审慎调整这些参数,并建议将会话存储切换为DbSession或CacheSession等共享存储,以保证会话数据在多个节点之间一致可用。
总结
Yii 2 通过Yii::$app->session与request/response组件的cookies集合,把原生 PHP 的$_SESSION、$_COOKIE全面对象化,提供了幂等的开关方法、数组风格的读写语法、自动打开的会话生命周期,以及一次性读取的闪存消息机制。在此基础上:
- 存储可插拔:
DbSession、CacheSession、redis\Session、mongodb\Session共享同一套 API,可在不改业务代码的前提下切换会话存储; - Cookie 防篡改:默认启用的 Cookie 验证 + 必须配置的
cookieValidationKey,让客户端篡改的 Cookie 无法通过request组件被读取; - 安全默认值:
httpOnly默认开启,sameSite自 2.0.21 起支持,配合secure标志与php.ini的session.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
相关推荐
Yii 2 框架会话与 Cookie 实战指南:从 $_SESSION 到面向对象封装、自定义存储与安全验证
Yii 2 框架会话与 Cookie 实战指南:从 $_SESSION 到面向对象封装、自定义存储与安全验证 导读 在 Yii 2 应用中,跨请求保持用户状态(
后端Web框架Yii 2 运行时指南:Session 与 Cookie 对象化封装、自定义存储与安全校验实战
Yii 2 运行时指南:Session 与 Cookie 对象化封装、自定义存储与安全校验实战 会话(Session)与 Cookie 是 Web 应用在多次请
后端Web框架Yii 2 会话(Session)与 Cookie 完全指南:面向对象的状态管理与 Cookie 签名验证
Yii 2 会话(Session)与 Cookie 完全指南:面向对象的状态管理与 Cookie 签名验证 会话(Session)与 Cookie 是 Web
后端Web框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考