在php中,cookie 和 session 的操作很简单,我们就不多说了。
在下面的文章中我们将分析一下二者在 yii2 中是如何实现的。
今天我们先来谈一谈 cookie。
说起 cookie ,从某层面上来说,这涉及以下两部分
服务端设置 cookie,即向客户端发送 cookie
客户端获取 cookie
在 yii2 中,这两部分的实现,也是分开的。
我们用 Response 组件描述服务端对 cookie 的设置,用 Request 组件描述客户端对 cookie 的获取等操作。
先看第一点,Response 对 cookie 的设置。
上篇文章,我们对 Response 做了介绍。
Response 组件并没有直接对 cookie 进行操作,cookie 的操作主要由 yii\web\CookieCollection 负责。
那 Response 充当了什么角色呢?
事实上, Response 组件的作用非常关键,虽然 yii\web\CookieCollection 负责管理cookie,比如添加/删除等。但对服务端而言,通过 setcookie 方法把 cookie 发送给客户端才有意义,Response 的作用即在于此。
通常,我们可以直接用 $response->cookies (如无特殊说明,后面介绍的 $reponse 都是 yii\web\Response 的实例)去描述 yii\web\CookieCollection 实例。
注意这里我们把 cookies 作为 $response 的属性去操作的。
有同学嚷嚷这一句就看不懂了,$response 并没有 cookies 公共属性,只有一个 _cookies 私有属性,你把话讲清楚了,这个是怎么描述的?
思考一下,如果你觉得这个问题刚好也是你的疑问,少年,前面的文中你真没认真的看呀!
事实上,$response->cookies 会间接的调用 $response->getCookies() 方法,Response::getCookies方法会返回 yii\web\CookieCollection 实例。
怎么间接调用的?不妨想一想我们讲过的魔术方法。
举一个添加cookie的例子1
2
3
4$response->cookies->add(new Cookie([
'name' => 'test_name',
'value' => 'This is cookie value',
]);
即调用 yii\web\CookieCollection::add 方法,向 yii\web\CookieCollection::_cookies 属性添加一个名叫 test_name 的 cookie。cookie 的单元信息,我们用 yii\web\Cookie 实例去描述,这只是描述cookie的基本属性,比如 name、value、httpOnly 等。
add 之后, yii\web\CookieCollection 的工作其实就结束了,并没有要求我们再手动写发送cookie 的逻辑代码。但是我们知道,这个过程 Response 组件帮我们做了。
上文我们介绍 Response 组件的时候,我们了解到,向客户端发送 cookie 信息,由 Response::sendCookies 方法提供,我们再来看一下这个过程(找到Response::sendCookies方法)。1
2
3if ($this->_cookies === null) {
return;
}
首先,获取 $response::_cookies 属性,这个属性的值,在我们刚刚 add 的过程中已经被赋值了,其值是 yii\web\CookieCollection 实例(从 yii\web\Response::getCookies 方法的实现上可以体现这一点)。
当然,如果 $response::_cookies 属性是 null,Response 组件就省了这一步。
Response::sendCookies 方法中,判断 Request::enableCookieValidation 这一段代码我们先不看,等会我再介绍。
我们先看一段比较有意思的 foreach1
2
3foreach ($this->getCookies() as $cookie) {
// ......
}
有同学要说了,上面刚刚强调了几遍,说 Response::getCookies 方法返回的是 yii\web\CookieCollection 实例,但是这里是循环这个对象?我没看错吧?
你没看错,是这么回事,思考一下怎么实现的?
之所以可以用 foreach 操作 yii\web\CookieCollection 实例,源于 yii\web\CookieCollection 类结构的特殊实现,我们来看一下1
2
3class CookieCollection extends Object implements \IteratorAggregate, \ArrayAccess, \Countable
{
}
正常情况下,你应该在看到这三个接口 IteratorAggregate,ArrayAccess 以及 Countable 就明白怎么回事了,估计有同学懵b中。。
这属于php层面的知识了,我们简单提一下,具体不懂的同学可以去查一下手册。
很明显这里的 foreach 表面上是循环 yii\web\CookieCollection 对象,其实循环的是 yii\web\CookieCollection::getIterator 方法的返回值。1
2
3
4public function getIterator()
{
return new ArrayIterator($this->_cookies);
}
它返回的是 ArrayIterator 迭代器遍历的数组,所以上面 foreach 肯定就没有问题了。
还记得我们刚刚调用 yii\web\CookieCollection::add 是是把cookie添加到哪里的吗?
以 name 为单元,保存在 yii\web\CookieCollection::_cookies 属性上。
而 ArrayIterator 迭代器处理的就是这个 cookie 集合,所以这个 foreach 最终循环的就是我们保存的 cookie 集合。
foreach 内的代码便不难理解了,获取 cookie 的值,通过 setcookie 函数向客户端发送 cookie 信息。
回过来看我们刚刚省略的代码段。1
2
3
4
5
6if ($request->enableCookieValidation) {
if ($request->cookieValidationKey == '') {
throw new InvalidConfigException(get_class($request) . '::cookieValidationKey must be configured with a secret key.');
$validationKey = $request->cookieValidationKey;
}
在 yii2 中,我们存储的 cookie ,默认是通过 hash 处理之后的内容,这就避免了某些潜在的风险。
我们知道,初始化 yii 应用之后,app/config/main-local.php 内都有对 request 组件的cookieValidationKey 属性进行过配置,而且其值往往都是一串乱七八糟的字符。
这个属性的配置很重要,它是我们 hash cookie 的密钥,从上面这个 if 判断可以看出,因为 Request::enableCookieValidation 属性默认为真,所以,默认情况,如果Request::cookieValidationKey 属性不配置的话,不仅无法设置 cookie ,而且还会通过 throw 抛出异常,当然你可以选择关闭 hash 。
具体的 hash 算法,由 security 组件提供。security 组件是核心组件,指的是 yii\base\Security ,该组件提供了HKDF、PBKDF2 以及很多加解密算法的实现,有兴趣的可以看看,后面我们就不对该组件单独介绍了。
额外需要提醒的是,默认设置的 cookie ,javascript 脚本是获取不到的。注意 setcookie 方法的最后一个参数,yii\web\Cookie::httpOnly 属性,默认为 true,你也可以在添加 cookie的时候设置该属性为 false ,以此确保可以在 javascript 中获取到,但是我们并不建议这样做。
来看第二点,cookie 的获取。
如果你想通过 $_COOKIE 的方式获取上面设置的 cookie ,不一定能获取到你想要的结果。因为这其中涉及到一个 hash 的处理。
所以,在框架内,我们建议使用上面的方式设置 cookie ,使用下面的方式来获取 cookie 。1
Yii::$app->request->cookies['test_name'];
还是同样的方式,Request::cookies 属性并不存在,只有一个 _cookies 私有属性,所以,Request::getCookies 方法会被调起。
Request::getCookies 方法部分代码如下1
2
3$this->_cookies = new CookieCollection($this->loadCookies(), [
'readOnly' => true,
]);
紧接着我们看一下 Request::loadCookies 方法的实现。
我们只看 hash 部分的代码,非 hash 部分的代码就没必要分析了。
同样,如果 Request::cookieValidationKey 属性为空,就会抛出异常。
我们主要看一下下面这段 foreach 的实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17foreach ($_COOKIE as $name => $value) {
if (!is_string($value)) {
continue;
}
$data = Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey);
if ($data === false) {
continue;
}
$data = @unserialize($data);
if (is_array($data) && isset($data[0], $data[1]) && $data[0] === $name) {
$cookies[$name] = new Cookie([
'name' => $name,
'value' => $data[1],
'expire' => null,
]);
}
}
循环 $_COOKIE,通过 security 组件,校验 $_COOKIE 的数据是否有效,即不是我们之前 hash 存储的数据或者无效了,过期了,统统过滤掉。
后面的代码就简单了,如果 unserialize 反序列化之后数据仍然有效,则将其转化为yii\web\Cookie单元,这便是整个获取 cookie 的过程。
正如我们开篇提到,无论是设置还是获取,并没有什么难度。
最后,需要了解一下 yii\web\CookieCollection 还提供了 cookie 的删除等操作,无非就是通过对 yii\web\CookieCollection::_cookies 属性的操作,最终通过 Response::sendCookies 方法通知客户端。
涉及到 cookie 的分析我们就介绍到这里,下一篇我们来看一下 session 的实现。