为了方便维护,Yii 封装了 cookie 的操作方法,在实现了常规 cookie 读写的基础上,还增加了 cookie 验证功能,用来防止 cookie 在客户端被修改。本文将详细解析 cookie 的实现过程。
参考类:
yii\base\Security - 哈希值生成和校验
yii\web\Cookie - 对 cookie 的封装
yii\web\CookieCollection - yii\web\Cookie 的集合
yii\web\Request - 读取客户端提交的 cookie
yii\web\Response - 向客户端发送 cookie
在 Yii 中,[[yii\web\Cookie]] 代表cookie,Web 请求和响应通过 [[yii\web\Request]], [[yii\web\Response]] 两个类来处理,而 cookie 的读取和发送也是在其中完成的。为了弄清 Yii 处理 cookie 的整个流程,下面通过实际场景逐步解析。
使用 [[yii\web\Response]] 发送 cookies
$cookies = Yii::$app->response->cookies;
$cookies->add(new \yii\web\Cookie([
'name' => 'language',
'value' => 'zh-CN',
]);
// 删除一个cookie
$cookies->remove('language');
// 等同于以下删除代码
unset($cookies['language']);
通过代码不难发现 [[yii\web\Response]] 通过一个 cookies 属性维护一个集合类
[[yii\web\CookieCollection]],写入 cookie 的方法是向这个集合中增加 [[yii\web\Cookie]] 。在 response 发送前,会调用 [[yii\web\Response::sendCookies()]] 方法,每个 cookie 在发送之前会经过哈希算法处理之后生成新的值,保证 cookie 无法在客户端修改。
// Method in yii\web\Response
protected function sendCookies()
{
if ($this->_cookies === null) {
return;
}
$request = Yii::$app->getRequest();
if ($request->enableCookieValidation) {
if ($request->cookieValidationKey == '') {
throw new InvalidConfigException(get_class($request) . '::cookieValidationKey must be configured with a secret key.');
}
$validationKey = $request->cookieValidationKey;
}
foreach ($this->getCookies() as $cookie) {
$value = $cookie->value;
if ($cookie->expire != 1 && isset($validationKey)) {
// 哈希处理
$value = Yii::$app->getSecurity()->hashData(serialize([$cookie->name, $value]), $validationKey);
}
setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly);
}
}
经过处理的 cookie 格式如下:
cd5c6293a1d65b62a2acdd426e230b588fda5a9e546f7d874d1b68e54642fcb1a%3A2%3A%7Bi%3A0%3Bs%3A8%3A%22language%22%3Bi%3A1%3Bs%3A5%3A%22zh-CN%22%3B%7D
如果经过哈希处理的 cookie 在客户端被修改将被yii\web\Request 过滤。
使用 [[Yii\web\Request]] 读取 cookies
$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'])) ...
和 [[yii\web\Response]] 一样 ,[[yii\web\Request]] 也通过 cookies 维护 CookieCollection ,服务端接收到的所有 cookie 会在验证后放进 cookies 中,以供读取。
// Method in yii\web\Request
protected function loadCookies()
{
$cookies = [];
if ($this->enableCookieValidation) {
if ($this->cookieValidationKey == '') {
throw new InvalidConfigException(get_class($this) . '::cookieValidationKey must be configured with a secret key.');
}
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,
]);
}
}
} else {
foreach ($_COOKIE as $name => $value) {
$cookies[$name] = new Cookie([
'name' => $name,
'value' => $value,
'expire' => null,
]);
}
}
return $cookies;
}
更多
- 如需关闭 cookie 验证,设置 [[yii\web\Request::enableCookieValidation]] 为 false,尽量不要这样做。
- 即便请求中的某个 cookie 未被验证,你仍然可以使用
$_COOKIE
访问这个未通过验证的cookie。 - 为防止 XSS 攻击,[[yii\web\Cookie]] 默认开启 httpOnly,此时cookie 无法被
Javascript 访问,如需关闭请设置 httpOnly 为 false 。
$cookie = new \yii\web\Cookie([
'name' => 'language',
'value' => 'zh-CN',
'httpOnly' => false,
])
参考文档:
https://github.com/yiisoft/yii2/blob/master/docs/guide/runtime-sessions-cookies.md