一、CSRF
即Cross-site request forgery跨站請(qǐng)求偽造琢唾,是指有人冒充你的身份進(jìn)行一些惡意操作兴蒸。
比如你登錄了網(wǎng)站A,網(wǎng)站A在你的電腦設(shè)置了cookie用以標(biāo)識(shí)身份和狀態(tài)裳瘪,然后你又訪問了網(wǎng)站B,這時(shí)候網(wǎng)站B就可以冒充你的身份在A網(wǎng)站進(jìn)行操作土浸,因?yàn)榫W(wǎng)站B在請(qǐng)求網(wǎng)站A時(shí),瀏覽器會(huì)自動(dòng)發(fā)送之前設(shè)置的cookie信息彭羹,讓網(wǎng)站A誤認(rèn)為仍然是你在進(jìn)行操作栅迄。
對(duì)于csrf的防范,一般都會(huì)放在服務(wù)器端進(jìn)行皆怕,那么我們來看下Yii2中是如何進(jìn)行防范的。
二西篓、Yii2 CSRF
首先說明一下愈腾,我安裝的是Yii2高級(jí)模版。
csrf token生成
vendor\yiisoft\yii2\web\Request.php
public function getCsrfToken($regenerate = false)
{
if ($this->_csrfToken === null || $regenerate) {
if ($regenerate || ($token = $this->loadCsrfToken()) === null) {
$token = $this->generateCsrfToken();
}
// the mask doesn't need to be very random
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.';
$mask = substr(str_shuffle(str_repeat($chars, 5)), 0, static::CSRF_MASK_LENGTH);
// The + sign may be decoded as blank space later, which will fail the validation
$this->_csrfToken = str_replace('+', '.', base64_encode($mask . $this->xorTokens($token, $mask)));
}
return $this->_csrfToken;
}
getCsrfToken方法首先會(huì)用loadCsrfToken方法嘗試加載已存在的token岂津,如果沒有則用generateCsrfToken方法再生成一個(gè)虱黄,并經(jīng)過后續(xù)處理,得到最終的前臺(tái)請(qǐng)求時(shí)攜帶的csrf token吮成。
protected function loadCsrfToken()
{
if ($this->enableCsrfCookie) {
return $this->getCookies()->getValue($this->csrfParam);
} else {
return Yii::$app->getSession()->get($this->csrfParam);
}
}
loadCsrfToken方法會(huì)嘗試從cookie或session中加載已經(jīng)存在的token橱乱,enableCsrfCookie默認(rèn)為true,所以一般會(huì)從cookie中獲取
public function getCookies()
{
if ($this->_cookies === null) {
$this->_cookies = new CookieCollection($this->loadCookies(), [
'readOnly' => true,
]);
}
return $this->_cookies;
}
這里又調(diào)用了loadCookies方法
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;
}
這里就是解析驗(yàn)證$_COOKIE中的數(shù)據(jù)粱甫。
cookies設(shè)置
vendor\yiisoft\yii2\web\Response.php
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);
}
}
sendCookies方法利用cookieValidationKey對(duì)cookie進(jìn)行一系列處理泳叠,主要是為了獲取的時(shí)候進(jìn)行驗(yàn)證,防止cookie被篡改茶宵。
public function getCookies()
{
if ($this->_cookies === null) {
$this->_cookies = new CookieCollection;
}
return $this->_cookies;
}
這里的getCookies方法跟request中的不同危纫,并不會(huì)從$_COOKIE中獲取,_cookies屬性在request中的generateCsrfToken方法中有進(jìn)行設(shè)置
protected function generateCsrfToken()
{
$token = Yii::$app->getSecurity()->generateRandomString();
if ($this->enableCsrfCookie) {
$cookie = $this->createCsrfCookie($token);
Yii::$app->getResponse()->getCookies()->add($cookie);
} else {
Yii::$app->getSession()->set($this->csrfParam, $token);
}
return $token;
}
csrf驗(yàn)證
vendor\yiisoft\yii2\web\Request.php
public function validateCsrfToken($token = null)
{
$method = $this->getMethod();
// only validate CSRF token on non-"safe" methods http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1
if (!$this->enableCsrfValidation || in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) {
return true;
}
$trueToken = $this->loadCsrfToken();
if ($token !== null) {
return $this->validateCsrfTokenInternal($token, $trueToken);
} else {
return $this->validateCsrfTokenInternal($this->getBodyParam($this->csrfParam), $trueToken)
|| $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken);
}
}
這里先驗(yàn)證一下請(qǐng)求方式,接著獲取cookie中的token种蝶,然后用validateCsrfTokenInternal方法進(jìn)行對(duì)比
private function validateCsrfTokenInternal($token, $trueToken)
{
if (!is_string($token)) {
return false;
}
$token = base64_decode(str_replace('.', '+', $token));
$n = StringHelper::byteLength($token);
if ($n <= static::CSRF_MASK_LENGTH) {
return false;
}
$mask = StringHelper::byteSubstr($token, 0, static::CSRF_MASK_LENGTH);
$token = StringHelper::byteSubstr($token, static::CSRF_MASK_LENGTH, $n - static::CSRF_MASK_LENGTH);
$token = $this->xorTokens($mask, $token);
return $token === $trueToken;
}
解析請(qǐng)求攜帶的csrf token 進(jìn)行對(duì)比并返回結(jié)果契耿。
三、總結(jié)
Yii2的做法就是先生成一個(gè)隨機(jī)token螃征,存入cookie中搪桂,同時(shí)在請(qǐng)求中攜帶隨機(jī)生成的csrf token,也是基于之前的隨機(jī)token而生成的盯滚,驗(yàn)證的時(shí)候?qū)ookie和csrf token進(jìn)行解析踢械,得到隨機(jī)token進(jìn)行對(duì)比,從而判斷請(qǐng)求是否合法淌山。
最后裸燎,本文只是對(duì)大概的流程進(jìn)行了分析,具體的細(xì)節(jié)還請(qǐng)查看源碼泼疑。