cookie

在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
3
if ($this->_cookies === null) {
return;
}

首先,获取 $response::_cookies 属性,这个属性的值,在我们刚刚 add 的过程中已经被赋值了,其值是 yii\web\CookieCollection 实例(从 yii\web\Response::getCookies 方法的实现上可以体现这一点)。

当然,如果 $response::_cookies 属性是 null,Response 组件就省了这一步。

Response::sendCookies 方法中,判断 Request::enableCookieValidation 这一段代码我们先不看,等会我再介绍。

我们先看一段比较有意思的 foreach

1
2
3
foreach ($this->getCookies() as $cookie) {
// ......
}

有同学要说了,上面刚刚强调了几遍,说 Response::getCookies 方法返回的是 yii\web\CookieCollection 实例,但是这里是循环这个对象?我没看错吧?

你没看错,是这么回事,思考一下怎么实现的?

之所以可以用 foreach 操作 yii\web\CookieCollection 实例,源于 yii\web\CookieCollection 类结构的特殊实现,我们来看一下

1
2
3
class CookieCollection extends Object implements \IteratorAggregate, \ArrayAccess, \Countable
{
}

正常情况下,你应该在看到这三个接口 IteratorAggregate,ArrayAccess 以及 Countable 就明白怎么回事了,估计有同学懵b中。。

这属于php层面的知识了,我们简单提一下,具体不懂的同学可以去查一下手册。

很明显这里的 foreach 表面上是循环 yii\web\CookieCollection 对象,其实循环的是 yii\web\CookieCollection::getIterator 方法的返回值。

1
2
3
4
public 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
6
if ($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
17
foreach ($_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 的实现。