yii2.0源码实现csrf验证

时间:2022-10-20 20:23:23
在yii\helpers\Html beginForm创建表单的时候,加入了csrf_token,name=_csrf-backend,type=hidden。这部分代码并不是很麻烦,这里就不再介绍了。下面主要介绍的是yii是怎么进行csrf验证的。

class Request
{
/**
* csrf token mask的长度
*/
const CSRF_MASK_LENGTH = 8;
private $_csrfToken;


/**
* @param bool $regenerate true每次都重新生成csrfToken,如果为true每次也都会生成token
* @return mixed 返回csrfToken
*/
public function getCsrfToken($regenerate = false)
{
if ($this->_csrfToken === null || $regenerate) {
//重新生成token或者从cookie或session中读取之前的token
if ($regenerate || ($token = $this->loadCsrfToken()) === null) {
$token = $this->generateCsrfToken();
}
// the mask doesn't need to be very random
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.';
/*
* 这里的static可以用self替换,php官网上解释5年以前是static当作self再用。
* str_repeat将目标字符串重复多少次
* str_shuffle随机打乱字符串顺序
*/
$mask = substr(str_shuffle(str_repeat($chars, 5)), 0, static::CSRF_MASK_LENGTH);


//base64_encode 使二进制数据可以通过非纯 8-bit 的传输层传输
$this->_csrfToken = str_replace('+', '.', base64_encode($mask . $this->xorTokens($token, $mask)));
}


return $this->_csrfToken;
}




/**
* 返回两个字符串异或的结果,返回的应该是byte类型的。
* @param $token1 $token
* @param $token2 $mask
* @return
*/
private function xorTokens($token1, $token2)
{
$n1 = StringHelper::byteLength($token1);
$n2 = StringHelper::byteLength($token2);
if ($n1 > $n2) {
$token2 = str_pad($token2, $n1, $token2);
} elseif ($n1 < $n2) {
$token1 = str_pad($token1, $n2, $n1 === 0 ? ' ' : $token1);
}


return $token1 ^ $token2;
}


/**
* 获取token
* @return mixed
*/
protected function loadCsrfToken()
{
if ($this->enableCsrfCookie) {
return $this->getCookies()->getValue($this->csrfParam);
} else {
return Yii::$app->getSession()->get($this->csrfParam);
}
}


/**
* 验证csrfToken
* @param $token
* @param $trueToken
* @return bool
*/
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;
}
}

整个的过程是:

通过session或者cookie里面保存的是csrf的明文token,通过一个随机的mask,以一定的规则(异或)加密token,同时将mask拼接到加密token上,因为异或操作使用到了byte,为了方便传输,需要base64_encode()生成新的csrf_token放到表单当中。
验证的时候,base64_decode()解码csrf_token,根据设定的mask长度截取csrf_token得到mask,然后再截取剩下的得到的是加密token,

mask与加密token做异或操作得到原来明文token(因为a异或b等于c,b异或c等于a),与session或cookie中的明文token验证。


上面我有一点不太懂的就是在操作base64_decode()得到的token的时候,为什么都是用mb_strlen(),mb_substr()。可能是异或生成的字符串的编码问题了。。。就写到这里吧,懂了我再更新好了。