zoukankan      html  css  js  c++  java
  • yii2 csrf验证原理分析

    知识补充

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

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

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

    字符

    二进制

    ASCII

    a

    1100001

    97

    b

    1100010

    98

    c

    1100011

    99

    d

    1100100

    100

    计算结果

    运算

    二进制

    ASCII

    a^b

    0000 0011

    3

    a^c

    0000 0010

    2

    b^d

    0000 0110

    6

    ab^cd

    0000 0010

    2

    a^cd

    0000 0010

    2

    ab^c

    0000 0010

    2

     

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

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

    <?php
    $str1='ab';
    $str2="cd";
    $r= $str1^$str2;
    var_dump($r);
    echo "<hr>";
    for($i=0;$i<strlen($r) ;$i++){
        echo ord($r[$i])."<br>";
    }
    ?>

    对于不等的
    以短的字符串长度位进行计算

    Yii2的csrf token验证
    在yii2的接收post请求时
    在如果开启
    enableCsrfValidation为true
    在/vendor/yiisoft/yii2/web/Controller.php

    <?php
       public function beforeAction($action)
        {
            if (parent::beforeAction($action)) {
                if ($this->enableCsrfValidation && Yii::$app->getErrorHandler()->exception === null && !Yii::$app->getRequest()->validateCsrfToken()) {
                    throw new BadRequestHttpException(Yii::t('yii', 'Unable to verify your data submission.'));
                }
                return true;
            }
            
            return false;
        }
    ?>

    会进行validateCsrfToken验证
    在/vendor/yiisoft/yii2/web/Request.php

    <?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);
            }
        }
    ?>

    说明在 GET, HEAD, OPTIONS 均不验证,除了这几种主要用的也就post了

    说明在我们发送post请求时必须发送相关验证的字段和值
    下面看CsrfToken产生过程
    在/vendor/yiisoft/yii2/web/Request.php里

    <?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;
        }
    ?>

    会发现
    _csrfToken的产生大致如下
    如果开启了enableCsrfCookie,
    CsrfToken就从cookie里取,否者从session里取(更安全)
    可在
    /vendor/yiisoft/yii2/web/Request.php的下面部位看到

    <?php
     protected function loadCsrfToken()
        {
            if ($this->enableCsrfCookie) {
                return $this->getCookies()->getValue($this->csrfParam);
            } else {
                return Yii::$app->getSession()->get($this->csrfParam);
            }
        }
    ?>

    从loadCsrfToken()里取出的值这里称token

    在post里发送的也就是Yii::$app->getRequest()->csrfParam 这里称csrfToken
    现在根据代码大致说下生成和验证的主要思路,当然自己看代码更能细致的了解
    1.从cookie或者session里取出token ,当然cookie或者session里如果没有就是初始化操作的过程了,这里初始化不是重点
    2.随机产生CSRF_MASK_LENGTH(Yii2里默认是8位)长度的字符串 mask
    3.对mask和token进行如下运算
    str_replace('+', '.', base64_encode($mask . $this->xorTokens($token, $mask)));
    
    $this->xorTokens($arg1,$arg2) 是一个先补位异或运算

    传入$arg1,$arg2
    长度短的要用自身补到长度长的字符串的位置
    见代码部分
    在 /vendor/yiisoft/yii2/web/Request.php 的如下部分

     <?php
      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;
        }
     ?>

    就是说如果 $arg1比$arg2短,$arg1要用自身补齐 补到和和$arg2一样的长度
    这里为什么要这样做?
    因为在php里
    'a'^'bc' 会只算 a^b 而不考虑c了,这里采用了向长度更长的来补
    如果用
    xorTokens来处理 'a'和'bc'
    会先把a用自己填充到和bc一样的长度后再进行异或运算
    异或运算详见上文补充

    str_replace('+', '.', base64_encode($mask . $this->xorTokens($token, $mask)));


    计算后即会得出在post请求时要发送的值 csrfToken

    下面是验证过程
    1.根据 表单字段名
    Yii::$app->getRequest()->csrfParam;
    从post里拿到
    csrfToken的值
    从方法 validateCsrfToken里可以看到
    代码
    在/vendor/yiisoft/yii2/web/Request.php 的如下部分

    <?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);
            }
        }
    
    ?>

    $this->getBodyParam($this->csrfParam)
    可以看出
    解密的目的就是要从
    csrfToken里取出token 然后和会话里的token比较
    见/vendor/yiisoft/yii2/web/Request.php 的如下部分

    <?php
     private function validateCsrfTokenInternal($token, $trueToken)
        {
    
            $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在加密过程中是xorTokens($trueToken,$mask)的结果
            */
            $token = $this->xorTokens($mask, $token);
    
            return $token === $trueToken;
        }
    ?>

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

    yii2
    中的核心思路
    token是从会话中取得的
    用随机串和token进行运算处理 得到一个加密串
    验证的时候通过这个加密串解密出来这个token和会话里的值进行比较

  • 相关阅读:
    storm源代码分析---Transactional spouts
    SQLServer 中存储过程
    实体添加映射
    SELECT INTO 和 INSERT INTO SELECT
    .NET System.Timers.Timer的原理和使用(开发定时执行程序)
    .net Framework 中的四种计时器
    AutoMapper 在你的项目里飞一会儿
    C#字符串、字节数组和内存流间的相互转换
    Entity Framework Code First使用者的福音 --- EF Power Tool使用记之二(问题探究)
    Entity Framework Code First使用者的福音 --- EF Power Tool使用记之一
  • 原文地址:https://www.cnblogs.com/HKUI/p/6068453.html
Copyright © 2011-2022 走看看