深入理解 cookie 在 Yii 2 中的实现原理

为了方便维护,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;
}

更多

  1. 如需关闭 cookie 验证,设置 [[yii\web\Request::enableCookieValidation]] 为 false,尽量不要这样做。
  2. 即便请求中的某个 cookie 未被验证,你仍然可以使用 $_COOKIE 访问这个未通过验证的cookie。
  3. 为防止 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

你可能感兴趣的:(深入理解 cookie 在 Yii 2 中的实现原理)