為了方便維護,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