当前位置:首页 > CMS教程 > 其它CMS > 列表

Yii2框架的csrf验证原理分析及token缓存解决方案

发布:smiling 来源: PHP粉丝网  添加日期:2020-04-05 17:35:19 浏览: 评论:0 

本文主要分三个部分,首先简单介绍csrf,接着对照源码重点分析一下yii框架的验证原理,最后针对页面缓存导致的token被缓存提出一种可行的方案。涉及的知识点会作为附录附于文末。感兴趣的朋友了解一下吧。

1.CSRF描述

CSRF全称为“Cross-Site Request Forgery”,是在用户合法的SESSION内发起的攻击。黑客通过在网页中嵌入Web恶意请求代码,并诱使受害者访问该页面,当页面被访问后,请求在受害者不知情的情况下以受害者的合法身份发起,并执行黑客所期待的动作。以下HTML代码提供了一个“删除产品”的功能:

  1. <a href="http://www.shop.com/delProducts.php?id=100" "javascript:return confirm('Are you sure?')">Delete</a> 

假设程序员在后台没有对该“删除产品”请求做相应的合法性验证,只要用户访问了该链接,相应的产品即被删除,那么黑客可通过欺骗受害者访问带有以下恶意代码的网页,即可在受害者不知情的情况下删除相应的产品。

2.yii的csrf验证原理 /vendor/yiisoft/yii2/web/Request.php简写为Request.php/vendor/yiisoft/yii2/web/Controller.php简写为Controller.php

开启csrf验证

在控制器里将enableCsrfValidation为true,则控制器内所有操作都会开启验证,通常做法是将enableCsrfValidation为false,而将一些敏感操作设为true,开启局部验证。

  1. public $enableCsrfValidation = false; 
  2.  
  3. /** 
  4.  
  5.  * @param \yii\base\Action $action 
  6.  
  7.  * @return bool 
  8.  
  9.  * @desc: 局部开启csrf验证(重要的表单提交必须加入验证,加入$accessActions即可 
  10.  
  11.  */ 
  12.  
  13. public function beforeAction($action){ 
  14.  
  15.     $currentAction = $action->id; 
  16.  
  17.     $accessActions = ['vote','like','delete','download']; 
  18.  
  19.     if(in_array($currentAction,$accessActions)) { 
  20.  
  21.         $action->controller->enableCsrfValidation = true; 
  22.  
  23.     } 
  24.  
  25.     parent::beforeAction($action); 
  26.  
  27.     return true; 
  28.  

生成token字段

在Request.php

首先通过安全组件Security获取一个32位的随机字符串,并存入cookie或session,这是原生的token.

  1. /** 
  2.  
  3.  * Generates  an unmasked random token used to perform CSRF validation. 
  4.  
  5.  * @return string the random token for CSRF validation. 
  6.  
  7.  */ 
  8.  
  9. protected function generateCsrfToken() 
  10.  
  11.  
  12.     $token = Yii::$app->getSecurity()->generateRandomString(); 
  13.  
  14.     if ($this->enableCsrfCookie) { 
  15.  
  16.         $cookie = $this->createCsrfCookie($token); 
  17.  
  18.         Yii::$app->getResponse()->getCookies()->add($cookie); 
  19.  
  20.     } else { 
  21.  
  22.         Yii::$app->getSession()->set($this->csrfParam, $token); 
  23.  
  24.     } 
  25.  
  26.     return $token
  27.  

接着通过一系列加密替换操作,生成加密后_csrfToken,这个是传给浏览器的token. 先随机产生CSRF_MASK_LENGTH(Yii2里默认是8位)长度的字符串 mask

对mask和token进行如下运算 str_replace('+', '.', base64_encode($mask . $this->xorTokens($token, $mask))); $this->xorTokens($arg1,$arg2) 是一个先补位异或运算

  1. /** 
  2.  
  3.  * Returns the XOR result of two strings. 
  4.  
  5.  * If the two strings are of different lengths, the shorter one will be padded to the length of the longer one. 
  6.  
  7.  * @param string $token1 
  8.  
  9.  * @param string $token2 
  10.  
  11.  * @return string the XOR result 
  12.  
  13.  */ 
  14.  
  15. private function xorTokens($token1$token2
  16.  
  17.  
  18.     $n1 = StringHelper::byteLength($token1); 
  19.  
  20.     $n2 = StringHelper::byteLength($token2); 
  21.  
  22.     if ($n1 > $n2) { 
  23.  
  24.         $token2 = str_pad($token2$n1$token2); 
  25.  
  26.     } elseif ($n1 < $n2) { 
  27.  
  28.         $token1 = str_pad($token1$n2$n1 === 0 ? ' ' : $token1); 
  29.  
  30.     } 
  31.  
  32.     return $token1 ^ $token2
  33.  
  34.  
  35. public function getCsrfToken($regenerate = false) 
  36.  
  37.  
  38.     if ($this->_csrfToken === null || $regenerate) { 
  39.  
  40.         if ($regenerate || ($token = $this->loadCsrfToken()) === null) { 
  41.  
  42.             $token = $this->generateCsrfToken(); 
  43.  
  44.         } 
  45.  
  46.         // the mask doesn't need to be very random 
  47.  
  48.         $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-.'
  49.  
  50.         $mask = substr(str_shuffle(str_repeat($chars, 5)), 0, static::CSRF_MASK_LENGTH); 
  51.  
  52.         // The + sign may be decoded as blank space later, which will fail the validation 
  53.  
  54.         $this->_csrfToken = str_replace('+''.'base64_encode($mask . $this->xorTokens($token$mask))); 
  55.  
  56.     } 
  57.  
  58.  
  59.  
  60.     return $this->_csrfToken; 
  61.  

验证token

在controller.php里调用request.php里的validateCsrfToken方法

  1. /** 
  2.  
  3.  * @inheritdoc 
  4.  
  5.  */ 
  6.  
  7. public function beforeAction($action
  8.  
  9.  
  10.     if (parent::beforeAction($action)) { 
  11.  
  12.         if ($this->enableCsrfValidation && Yii::$app->getErrorHandler()->exception === null && !Yii::$app->getRequest()->validateCsrfToken()) { 
  13.  
  14.             throw new BadRequestHttpException(Yii::t('yii''Unable to verify your data submission.')); 
  15.  
  16.         } 
  17.  
  18.         return true; 
  19.  
  20.     } 
  21.  
  22.       
  23.  
  24.     return false; 
  25.  
  26.  
  27. public function validateCsrfToken($token = null) 
  28.  
  29.  
  30.     $method = $this->getMethod(); 
  31.  
  32.     if (!$this->enableCsrfValidation || in_array($method, ['GET''HEAD''OPTIONS'], true)) { 
  33.  
  34.         return true; 
  35.  
  36.     } 
  37.  
  38.  
  39.  
  40.     $trueToken = $this->loadCsrfToken();//如果开启了enableCsrfCookie,CsrfToken就从cookie里取,否者从session里取(更安全) 
  41.  
  42.  
  43.  
  44.     if ($token !== null) { 
  45.  
  46.         return $this->validateCsrfTokenInternal($token$trueToken); 
  47.  
  48.     } else { 
  49.  
  50.         return $this->validateCsrfTokenInternal($this->getBodyParam($this->csrfParam), $trueToken
  51.  
  52.             || $this->validateCsrfTokenInternal($this->getCsrfTokenFromHeader(), $trueToken); 
  53.  
  54.     } 
  55.  

获取客户端传入

$this->getBodyParam($this->csrfParam)

然后是validateCsrfTokenInternal

  1. private function validateCsrfTokenInternal($token$trueToken
  2.  
  3.  
  4.     if (!is_string($token)) { 
  5.  
  6.         return false; 
  7.  
  8.     } 
  9.  
  10.     $token = base64_decode(str_replace('.''+'$token)); 
  11.  
  12.     $n = StringHelper::byteLength($token); 
  13.  
  14.     if ($n <= static::CSRF_MASK_LENGTH) { 
  15.  
  16.         return false; 
  17.  
  18.     } 
  19.  
  20.     $mask = StringHelper::byteSubstr($token, 0, static::CSRF_MASK_LENGTH); 
  21.  
  22.     $token = StringHelper::byteSubstr($tokenstatic::CSRF_MASK_LENGTH, $n - static::CSRF_MASK_LENGTH); 
  23.  
  24.     $token = $this->xorTokens($mask$token); 
  25.  
  26.  
  27.  
  28.     return $token === $trueToken
  29.  

加密时用的是 str_replace('+', '.', base64_encode(mask.mask.this->xorTokens(token,token,mask))); 解密 1.首先要把.替换成+ 2.然后base64_decode 再 根据长度分别取出mask和mask和this->xorTokens(token,token,mask) ; 为了说明方便 this−>xorTokens(this−>xorTokens(token, $mask) 这里称作 token1 然后 进行mask和token1的异或运算,即得token 注意在加密时

token1=token^mask

所以 解密时

token=mask^token1=mask^(token^mask)

3.token缓存的解决方案

当页面整体被缓存后,token也被缓存导致验证失败,一种常见的解决思路是每次提交前重新获取token,这样就可以通过验证了。

附录:str_pad(),该函数返回 input 被从左端、右端或者同时两端被填充到制定长度后的结果。如果可选的 pad_string 参数没有被指定,input 将被空格字符填充,否则它将被 pad_string 填充到指定长度;

str_shuffle() 函数打乱一个字符串,使用任何一种可能的排序方案。

因为yii2 csrf的验证的加解密 涉及到异或运算

所以需要先补充php里字符串异或运算的相关知识,不需要的可以跳过

^异或运算 不一样返回1 否者返回 0 在PHP语言中,经常用来做加密的运算,解密也直接用^就行 字符串运算时 利用字符的ascii码转换为2进制来运算 单个字符运算

1.对于单个字符和单个字符的 直接计算其结果即可 比如表里的a^b

2.对于长度一样的多个字符串 如表里的ab^cd 计算a^c对应的结果和和b^d对应的结果 对应的字符连接起来

Tags: Yii2框架 csrf验证原理 token

分享到: