当前位置:  开发笔记 > 编程语言 > 正文

Yii2 中CSRF的疑问(完结)

描述你的问题在开启Csrf防御的时候,默认csrf的值只能使用一次,第二次提交就是验证不通过,因为已经使用过了,那么如何做下面这种效果呢?如图,该页面是Ajax提交请求,那么第一个按钮点击后csrf值失效了,第二
  1. 描述你的问题
    在开启Csrf防御的时候,默认csrf的值只能使用一次,第二次提交就是验证不通过,因为已经使用过了,那么如何做下面这种效果呢?

如图,该页面是Ajax提交请求,那么第一个按钮点击后csrf值失效了,第二次提交失败返回400错误,求解!
怎么让csrf的值可以刷新后用于第二次请求。

不要让我关闭csrf,csrf本来就是为了防御这种请求的。

经过楼下兄弟的指点,现分析如下,
在使用他自带的表单的时候,\yii\helpers\BaseHtml::beginForm 方法中有判断,如果在一次页面展示中多次调用表单创建,则每次获取的csrf都是一样的,也就是说,同一页面多个表单,都可用的。
我们知道csrf验证是在\yii\web\Request类中,但是该类在初始化的时候我并没有看到他有检查csrf,所以我们需要知道他是在哪里进行检查的,通过全文搜索,发现在\yii\web\Controller类的beforeAction方法中有检查,原型如下:

/**
     * @inheritdoc
     */
    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;
    }

通过该代码我们知道,在检查csrf是直接使用的\yii\web\Request::validateCsrfToken方法,在该方法中先通过loadCsrfToken方法获取csrf,

/**
     * Loads the CSRF token from cookie or session.
     * @return string the CSRF token loaded from cookie or session. Null is returned if the cookie or session
     * does not have CSRF token.
     */
    protected function loadCsrfToken()
    {
        if ($this->enableCsrfCookie) {
            return $this->getCookies()->getValue($this->csrfParam);
        } else {
            return Yii::$app->getSession()->get($this->csrfParam);
        }
    }

这个代码很好理解,就是cookie取,没开的话就在session取,然后返回。
然后通过validateCsrfTokenInternal方法验证csrf token,通过跟踪代码发现,在验证完csrf后并没有删除该csrf,换句话说就是该csrf还是可以用的,只要你页面不刷新,因为页面刷新时在 你布局文件的头部会有个 用来输出csrf,这时候上一个csrf就会失效的。所以说一个页面不停地AJAX请求,只要不刷新是没问题的。
结论:
在yii中一共有如下几个地方会刷新csrf
\yii\helpers\BaseHtml::beginFormHtml::csrfMetaTags()\yii\web\Request::getCsrfToken,关键点在\yii\web\Request::getCsrfToken方法里如楼下兄弟所言,$regenerate 参数也可以控制强制重新生成csrf token.

private $_csrfToken;

    /**
     * Returns the token used to perform CSRF validation.
     *
     * This token is a masked version of [[rawCsrfToken]] to prevent [BREACH attacks](http://breachattack.com/).
     * This token may be passed along via a hidden field of an HTML form or an HTTP header value
     * to support CSRF validation.
     * @param boolean $regenerate whether to regenerate CSRF token. When this parameter is true, each time
     * this method is called, a new CSRF token will be generated and persisted (in session or cookie).
     * @return string the token used to perform CSRF validation.
     */
    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的时候判断 $this->_csrfToken是否是空的,按照页面正常加载来说程序一启动肯定是空的,这个值不是存客户端的,这个也不是从服务端获取的,是页面启动后就是空的,只有执行该方法后生成的csrf token才会放入cookiesession,也就是说在同一个请求中多次执行该方法,获取的csrf token都是一样的,如果一个新页面请求到该方法就会重新生成,这也是不同页面刷新为何csrf token一直变的原因,所以,csrf token值可多次使用,但是只限当前页面的ajax请求。

我的系统问题是由于我使用的是IIS 10作为Web,又开了Url重写,默认不存在的文件或文件夹都会重写给Yii处理,然而我没有在web目录下放置传说中的favicon.ico文件,导致有个404,这个是浏览器在后台自动请求的,导致yii输出了一个404,而我不知道,404页面又调用了布局,导致csrf token 被刷新了。很二是吧。。。。。。

回复内容:

  1. 描述你的问题
    在开启Csrf防御的时候,默认csrf的值只能使用一次,第二次提交就是验证不通过,因为已经使用过了,那么如何做下面这种效果呢?

如图,该页面是Ajax提交请求,那么第一个按钮点击后csrf值失效了,第二次提交失败返回400错误,求解!
怎么让csrf的值可以刷新后用于第二次请求。

不要让我关闭csrf,csrf本来就是为了防御这种请求的。

经过楼下兄弟的指点,现分析如下,
在使用他自带的表单的时候,\yii\helpers\BaseHtml::beginForm 方法中有判断,如果在一次页面展示中多次调用表单创建,则每次获取的csrf都是一样的,也就是说,同一页面多个表单,都可用的。
我们知道csrf验证是在\yii\web\Request类中,但是该类在初始化的时候我并没有看到他有检查csrf,所以我们需要知道他是在哪里进行检查的,通过全文搜索,发现在\yii\web\Controller类的beforeAction方法中有检查,原型如下:

/**
     * @inheritdoc
     */
    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;
    }

通过该代码我们知道,在检查csrf是直接使用的\yii\web\Request::validateCsrfToken方法,在该方法中先通过loadCsrfToken方法获取csrf,

/**
     * Loads the CSRF token from cookie or session.
     * @return string the CSRF token loaded from cookie or session. Null is returned if the cookie or session
     * does not have CSRF token.
     */
    protected function loadCsrfToken()
    {
        if ($this->enableCsrfCookie) {
            return $this->getCookies()->getValue($this->csrfParam);
        } else {
            return Yii::$app->getSession()->get($this->csrfParam);
        }
    }

这个代码很好理解,就是cookie取,没开的话就在session取,然后返回。
然后通过validateCsrfTokenInternal方法验证csrf token,通过跟踪代码发现,在验证完csrf后并没有删除该csrf,换句话说就是该csrf还是可以用的,只要你页面不刷新,因为页面刷新时在 你布局文件的头部会有个 用来输出csrf,这时候上一个csrf就会失效的。所以说一个页面不停地AJAX请求,只要不刷新是没问题的。
结论:
在yii中一共有如下几个地方会刷新csrf
\yii\helpers\BaseHtml::beginFormHtml::csrfMetaTags()\yii\web\Request::getCsrfToken,关键点在\yii\web\Request::getCsrfToken方法里如楼下兄弟所言,$regenerate 参数也可以控制强制重新生成csrf token.

private $_csrfToken;

    /**
     * Returns the token used to perform CSRF validation.
     *
     * This token is a masked version of [[rawCsrfToken]] to prevent [BREACH attacks](http://breachattack.com/).
     * This token may be passed along via a hidden field of an HTML form or an HTTP header value
     * to support CSRF validation.
     * @param boolean $regenerate whether to regenerate CSRF token. When this parameter is true, each time
     * this method is called, a new CSRF token will be generated and persisted (in session or cookie).
     * @return string the token used to perform CSRF validation.
     */
    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的时候判断 $this->_csrfToken是否是空的,按照页面正常加载来说程序一启动肯定是空的,这个值不是存客户端的,这个也不是从服务端获取的,是页面启动后就是空的,只有执行该方法后生成的csrf token才会放入cookiesession,也就是说在同一个请求中多次执行该方法,获取的csrf token都是一样的,如果一个新页面请求到该方法就会重新生成,这也是不同页面刷新为何csrf token一直变的原因,所以,csrf token值可多次使用,但是只限当前页面的ajax请求。

我的系统问题是由于我使用的是IIS 10作为Web,又开了Url重写,默认不存在的文件或文件夹都会重写给Yii处理,然而我没有在web目录下放置传说中的favicon.ico文件,导致有个404,这个是浏览器在后台自动请求的,导致yii输出了一个404,而我不知道,404页面又调用了布局,导致csrf token 被刷新了。很二是吧。。。。。。

如果你在进行完一次请求没有手动去调用Yii::$app->request->getCsrfToken(true)的话是不会存在你说的验证不通过的问题的

推荐阅读
雯颜哥_135
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有