yii2的csrf验证原理分析与token缓存解决方案

本文主要分三单部分,首先简单介绍csrf,接着对照源码重点分析一下yii框架的证明原理,最后对页面缓存导致的token被缓存提出同样栽有效的方案。涉及的知识点会作为附录附于文末。

前言

筹一个缓存系统,不得不要考虑的题材即是:缓存穿透、缓存击穿与失效时之雪崩效应。

1.CSRF描述

缓存穿透

缓存穿透是因查询一个定非存在的数额,由于缓存是不命中时被动写的,并且由于容错考虑,如果打存储层查不顶数码则免写副缓存,这将导致这个不存在的数每次要都要交囤层去查询,失去了缓存的含义。在流量大时,可能DB就挂掉了,要是有人利用非存在的key频繁攻击我们的使用,这就算是漏洞。

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

解决方案

出很多种主意可中地缓解缓存穿透问题,最普遍的虽然是使用布隆过滤器,将富有可能在的多少哈希及一个足够好的bitmap中,一个定非在的数据会被
这个bitmap拦截掉,从而避免了针对根存储系统的询问压力。另外为时有发生一个进一步简易粗暴的法门(我们采取的即是这种),如果一个查询返回的多少为空(不管是累
据不存,还是系统故障),我们照样将这个拖欠结果开展缓存,但它的过期时会见格外缺乏,最丰富无越五分钟。

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

缓存雪崩

缓存雪崩是依赖于咱们设置缓存时以了千篇一律的逾期时,导致缓存在有平随时以失效,请求全部倒车到DB,DB瞬时压力过重雪崩。

若果程序员在后台从未对该“删除产品”请求做相应的合法性验证,只要用户访问了拖欠链接,相应的产品即让剔除,那么黑客可经过欺骗受害者访问带有以下恶意代码的网页,即可在受害人不知情的情事下删除相应的活。

缓解方案

缓存失效时的雪崩效应对根系统的打非常可怕。大多数体系设计者考虑就此加锁或者队列的方确保缓存的单线
程(进程)写,从而避免失效时大量的起请求落到脚存储系统上。这里分享一个简单易行方案虽三天两头讲话缓存失效时分散开,比如我们好于原来的失灵时基础及长一个随意值,比如1-5分钟即兴,这样各个一个缓存的逾期时的重复率就会见骤降,就可怜为难吸引公共失效的波。

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

缓存击穿

对此片安了过时的key,如果这些key可能会见于一些日子接触让超过高并发地访问,是一模一样栽颇“热点”的数据。这个时,需要考虑一个题目:缓存被“击穿”的问题,这个跟缓存雪崩的区分在于这里针对某个平等key缓存,前者则是无数key。

苏存在有时间点过期的时光,恰好在这个时间接触对这Key有大量的出现请求过来,这些请求发现缓存过期一般还见面由后端DB加载数据并回设到缓存,这个时挺出现的伸手或会见转将后端DB压垮。

/vendor/yiisoft/yii2/web/Controller.php简写为Controller.php

釜底抽薪方案

开启csrf验证

1.动互斥锁(mutex key)

业界比较常用的做法,是使用mutex。简单地以来,就是以缓存失效的下(判断用出来的价为空),不是就去load
db,而是先采用缓存工具的某些带成操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex
key,当操作返回成功时,再展开load
db的操作并回设缓存;否则,就重试整个get缓存的措施。

SETNX,是「SET if Not
eXists」的缩写,也尽管是只有无在的时节才装,可以利用其来贯彻锁的效用。在redis2.6.1事先版本未兑现setnx的超时时,所以这里给起片栽版本代码参考:

//2.6.1前单机版本锁
String get(String key) {  
   String value = redis.get(key);  
   if (value  == null) {  
    if (redis.setnx(key_mutex, "1")) {  
        // 3 min timeout to avoid mutex holder crash  
        redis.expire(key_mutex, 3 * 60)  
        value = db.get(key);  
        redis.set(key, value);  
        redis.delete(key_mutex);  
    } else {  
        //其他线程休息50毫秒后重试  
        Thread.sleep(50);  
        get(key);  
    }  
  }  
}

新式版本代码:

public String get(key) {
      String value = redis.get(key);
      if (value == null) { //代表缓存值过期
          //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
          if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功
               value = db.get(key);
                      redis.set(key, value, expire_secs);
                      redis.del(key_mutex);
              } else {  //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
                      sleep(50);
                      get(key);  //重试
              }
          } else {
              return value;      
          }
 }

memcache代码:

if (memcache.get(key) == null) {  
    // 3 min timeout to avoid mutex holder crash  
    if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
        value = db.get(key);  
        memcache.set(key, value);  
        memcache.delete(key_mutex);  
    } else {  
        sleep(50);  
        retry();  
    }  
} 

以控制器里以enableCsrfValidation为true,则控制器内所有操作都见面敞开验证,通常做法是将enableCsrfValidation为false,而将部分机警操作设为true,开启局部验证。

2. “提前”使用互斥锁(mutex key):

于value内部设置1只超时值(timeout1), timeout1比实际的memcache
timeout(timeout2)小。当起cache读博到timeout1发现它们已晚点时,马上延长timeout1并更安及cache。然后再度由数据库加载数据并安装到cache中。伪代码如下:

v = memcache.get(key);  
if (v == null) {  
    if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
        value = db.get(key);  
        memcache.set(key, value);  
        memcache.delete(key_mutex);  
    } else {  
        sleep(50);  
        retry();  
    }  
} else {  
    if (v.timeout <= now()) {  
        if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
            // extend the timeout for other threads  
            v.timeout += 3 * 60 * 1000;  
            memcache.set(key, v, KEY_TIMEOUT * 2);  

            // load the latest value from db  
            v = db.get(key);  
            v.timeout = KEY_TIMEOUT;  
            memcache.set(key, value, KEY_TIMEOUT * 2);  
            memcache.delete(key_mutex);  
        } else {  
            sleep(50);  
            retry();  
        }  
    }  
} 
public $enableCsrfValidation = false;
/**
 * @param \yii\base\Action $action
 * @return bool
 * @desc: 局部开启csrf验证(重要的表单提交必须加入验证,加入$accessActions即可
 */
public function beforeAction($action){
    $currentAction = $action->id;
    $accessActions = ['vote','like','delete','download'];
    if(in_array($currentAction,$accessActions)) {
        $action->controller->enableCsrfValidation = true;
    }
    parent::beforeAction($action);
    return true;
}

3. “永远不超时”:  

这里的“永远不超时”包含两重叠意思:

(1)
从redis上看,确实尚未设置过时,这虽保证了,不见面面世热点key过期问题,也就是“物理”不超时。

(2)
从效益上看,如果未超时,那不就是变成静态的了啊?所以我们将过日有key对应的value里,如果发现而过了,通过一个后台的异步线程进行缓存的构建,也就算是“逻辑”过期

       
从实战看,这种方式对于性特别友善,唯一不足之即是构建缓存时候,其余线程(非构建缓存的线程)可能看的是一味多少,但是对于一般的互联网力量来说这要得以经。

String get(final String key) {  
        V v = redis.get(key);  
        String value = v.getValue();  
        long timeout = v.getTimeout();  
        if (v.timeout <= System.currentTimeMillis()) {  
            // 异步更新后台异常执行  
            threadPool.execute(new Runnable() {  
                public void run() {  
                    String keyMutex = "mutex:" + key;  
                    if (redis.setnx(keyMutex, "1")) {  
                        // 3 min timeout to avoid mutex holder crash  
                        redis.expire(keyMutex, 3 * 60);  
                        String dbValue = db.get(key);  
                        redis.set(key, dbValue);  
                        redis.delete(keyMutex);  
                    }  
                }  
            });  
        }  
        return value;  
}

生成token字段

4. 资源保护:

下netflix的hystrix,可以开资源的隔断保护主线程池,如果将此以到缓存的构建也未尝不可。

季种缓解方案:没有最佳只有最确切

解决方案 优点 缺点
简单分布式互斥锁(mutex key)

 1. 思路简单

2. 保证一致性

1. 代码复杂度增大

2. 存在死锁的风险

3. 存在线程池阻塞的风险

“提前”使用互斥锁  1. 保证一致性 同上 
不过期(本文)

1. 异步构建缓存,不会阻塞线程池

1. 不保证一致性。

2. 代码复杂度增大(每个value都要维护一个timekey)。

3. 占用一定的内存空间(每个value都要维护一个timekey)。

资源隔离组件hystrix(本文)

1. hystrix技术成熟,有效保证后端。

2. hystrix监控强大。

 

 

1. 部分访问存在降级策略。

季种植方案来网络,详文请链接:http://carlosfu.iteye.com/blog/2269687?hmsr=toutiao.io&utm\_medium=toutiao.io&utm\_source=toutiao.io

在Request.php

总结

本着工作系统,永远都是具体情况具体分析,没有最好,只有极当。

末尾,对于缓存系统广大的休养生息存满了同数量丢失问题,需要根据实际业务分析,通常咱们下LRU策略处理溢起,Redis的RDB和AOF持久化策略来管得情况下的多寡安全。

先是通过安全组件Security获取一个32各类之自由字符串,并存入cookie或session,这是原生的token.

/**
 * Generates  an unmasked random token used to perform CSRF validation.
 * @return string the random token for CSRF validation.
 */
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;
}

跟着通过平等多重加密替换操作,生成加密后_csrfToken,这个是传于浏览器的token.
先随机产生CSRF_MASK_LENGTH(Yii2里默认是8号)长度的字符串 mask

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

/**
 * Returns the XOR result of two strings.
 * If the two strings are of different lengths, the shorter one will be padded to the length of the longer one.
 * @param string $token1
 * @param string $token2
 * @return string the XOR result
 */
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;
}
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;
}

验证token

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

/**
 * @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;
}
public function validateCsrfToken($token = null)
{
    $method = $this->getMethod();
    if (!$this->enableCsrfValidation || in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) {
        return true;
    }

    $trueToken = $this->loadCsrfToken();//如果开启了enableCsrfCookie,CsrfToken就从cookie里取,否者从session里取(更安全)

    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)

然后是validateCsrfTokenInternal

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

加密时用的凡 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对应之结果 对应之字符连接起来

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图