135. 洪水控制防护flood

    你的网站可能有一些很重的操作,比如给客户提供价格查询,产品数量达到了数千万,此时查询一次需要消耗很多的系统资源,如果有恶意攻击者频繁发起查询,那么系统会出现严重的性能问题,此时你需要一个进行频率限制的机制,限定一个用户在设定时间段内最多执行多少次查询;频率限制还有很多用处,比如防止用户进行大量登录尝试以猜测密码,此时应在设定时间段内错误次数达到某个阀值就拒绝登录,再比如邮件或短信的发送频率限制等等,本质上频率限制就是进行洪水控制的。

洪水控制器:

Drupal中洪水控制器就是实现以上频率限制机制的一个工具,服务定义如下:

服务idflood

类:Drupal\Core\Flood\DatabaseBackend

获取方法:\Drupal::flood();

用于控制一些事情在一定时间段内被执行的最大次数。在实现上该服务将事务执行记录保存到数据库中,通过分析这些记录以判断是否频率过高,数据库表为“flood”,各字段含义如下:

fid

事件id,整数,自增,主键

event

事件名称,字符串,最长64字符

identifier

用户唯一标识符,字符串,最长128,用于唯一标识一个用户,通常为ip,或用户账号

timestamp

事件发生的时间戳

expiration

保存记录的到期时间戳,超过该时间记录会被计划任务自动清除

 

方法介绍:

public function register($name, $window = 3600, $identifier = NULL)

将一个需要进行频率控制的事件记录到数据库中,该事件每次执行后都应该调用该方法进行记录,这样才能分析频率,参数如下:

$name

字符串值,事件名称,为避免冲突建议以模块名做前缀,如mymodule.my_event

$window

本条记录有效时间长度,以秒为单位的一个整数,默认为3600(一小时),表示从当前请求时间开始计算,超过该时间段后,记录会被计划任务清理

$identifier

用户唯一标识符,默认为null,此时采用客户端ip

 

public function clear($name, $identifier = NULL)

清除一条事件记录,参数含义和register方法一样

 

public function isAllowed($name, $threshold, $window = 3600, $identifier = NULL)

判断事件次数是否达到阀值,如果没有,返回true,表示允许事件再次发生,否则返回false,参数如下:

$name

事件名,同register方法一样

$threshold

一个整数,表示事件次数阀值,当大于等于该值时将返回false

$window

一个以秒为单位的整数,默认为3600秒,表示从当前请求向前的该时间段内事件是否达到阀值,该参数虽然名字和register方法一样但含义却不相同,在该方法中表示阀值判断的时间段,而在register方法中表示记录有效期,前者应小于后者,通常采用相同的值。

$identifier

用户唯一标识符,同register方法一样,调用时应采用相同的值

 

public function garbageCollection()

清理失效记录,超期时间小于当前请求时间的记录会被清理,该方法在计划任务中执行,详见系统模块的计划任务函数:system_cron()

 

内存型洪水控制器:

类:\Drupal\Core\Flood\MemoryBackend

这被用来在单个请求中进行洪水控制,数据记录在内存中,不能跨请求存在,接口相同

 

关于用户唯一标识符:

该标识符的选取应依据实际情况而定,登录用户推荐用账户id,对于匿名用户而言,通常选择ip即可,但如果你使用了固定ip的反向代理,此时需要确保该标识符为用户真实ip;现在有很多局域网用户共享相同的公网ip,你可能会想到为了进一步精确标识用户,而在标识符中加入用户代理信息(浏览器标识符),这样确实能够精确标识,但是却为攻击者提供了绕开的方法,使得洪水攻击形同虚设,这源于在HTTP协议中用户代理信息是由客户端设置的,通常攻击者使用的工具并不是浏览器,而是直接编写面向HTTP协议的程序,这样在每次请求中设置不同的代理信息就能轻易绕开频率限制,而客户端ip是由系统从TCP数据包中提取的,难以被伪造,所以相对安全。

 

在系统中的应用:

这被用在了登录限制中,默认一小时内密码错误达到50次,即暂时不允许登录,详见以下配置对象:

\Drupal::config('user.flood');

除此外还被用在电商模块commerce、联系表单模块contact等地方

 

文件型洪水控制器:

这里提供一个文件类型的洪水控制器,是多年前云客因项目需要而写,和drupal无关,是一个独立运用的类库,仅供参考:


<?php

/**
 * 根据客户端ip限制用户访问频率,数据存放在文件系统中
 * 默认不允许使用代理,在允许使用代理时限制可能被绕过
 * by:yunke
 */
class clientAccessLimit
{
    protected $defualt_option = array(
        "dir"          => "yunke", //储存数据的目录名
        "path"         => '', //储存数据的目录路径,默认网站根目录
        "allowProxy"   => false, //禁止代理 忽略代理服务器ip
        "timeInterval" => 3600, //时间间隔 1小时
        "maxlimit"     => 100, //在间隔期内最大允许访问量,默认每小时只能访问100次
        "cacheTime"    => 172800, //限制数据保存时间 默认两天 60 * 60 * 24 * 2
    );
    protected $option = array();
    protected $dir;
    protected $iPFlag;
    protected $currentTime;

    /**
     * 可传递选项数组以改变该类行为
     *
     * @param array $option
     */
    public function __construct($option = array())
    {
        $this->defualt_option["path"] = $_SERVER['DOCUMENT_ROOT'];
        $option += $this->defualt_option;
        $option['path'] = rtrim($option['path'], '\\\/');
        $option['dir'] = rtrim($option['dir'], '\\\/');
        $dir = $option['path'] . "/" . $option['dir'];
        if (!is_dir($dir)) {
            @mkdir($dir, 0777, true);
        }
        if ($option['timeInterval'] > $option['cacheTime']) {
            throw new \LogicException("间隔时间必须小于文件缓存时间");
        }
        $this->option = $option;
        $this->dir = $dir . "/";
        $this->currentTime = time();
        $this->clear();
        $this->getIPFlag();
    }

    /**
     * 返回true代表需要限制,不应该继续操作,
     * 返回false代表可以继续操作,不受限制
     *
     * @return bool
     */
    public function isLimit()
    {
        $data = $this->getData();
        if (empty($data)) {
            $data = array($this->currentTime);
            $this->setData($data);
            return false;
        }
        $start_time = $this->currentTime - $this->option['timeInterval'];
        $newData = array();
        foreach ($data as $time) {
            if ($time >= $start_time) {
                $newData[] = $time;
            }
        }

        $limit = count($newData) >= $this->option['maxlimit'] ? true : false;
        if (!$limit) {
            $newData[] = $this->currentTime;
        }
        $this->setData($newData);
        return $limit;
    }

    /**
     * 得到用户ip旗标,用作文件名
     */
    protected function getIPFlag()
    {
        $ipFlag = "ip_" . (string)$_SERVER['REMOTE_ADDR']; //来源于tcp数据包,不易伪造

        if ($this->option['allowProxy']) {
            //代理地址可伪造
            if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
                $ipFlag .= (string)$_SERVER['HTTP_X_FORWARDED_FOR'];
            }
            if (isset($_SERVER['HTTP_CLIENT_IP'])) {
                $ipFlag .= (string)$_SERVER['HTTP_CLIENT_IP'];
            }
        }
        $this->iPFlag = substr($ipFlag, 0, 255);
    }

    /**
     * 得到用户历史访问数据
     *
     * @return array|mixed
     */
    protected function getData()
    {
        $file = $this->dir . $this->iPFlag;
        if (!file_exists($file)) {
            return array();
        }
        return unserialize(file_get_contents($file));
    }

    /**
     * 保存用户访问数据
     * 访问数据为一个由历次访问时间构成的数组
     *
     * @param array $data
     */
    protected function setData($data = array())
    {
        $file = $this->dir . $this->iPFlag;
        $data = serialize($data);
        @file_put_contents($file, $data);
    }

    /**
     * 清理过期数据,防止无用数据堆积
     *
     * @return bool
     */
    protected function clear()
    {
        $cache = $this->dir . "cache";
        if (!file_exists($cache)) {
            @file_put_contents($cache, $this->currentTime);
            return true;
        }
        $lastClearTime = (int)file_get_contents($cache);
        $isNeedClear = ($this->currentTime - $lastClearTime) > $this->option['cacheTime'] ? true : false;
        if (!$isNeedClear) {
            return true;
        }
        //执行过期数据清理 迭代文件
        $files = scandir($this->dir);
        foreach ($files as $file) {
            if (strpos($file, "ip_") !== 0) {
                continue;
            }
            $isNeedDlete = ($this->currentTime - filectime($this->dir . $file)) > $this->option['cacheTime'] ? true : false;
            if ($isNeedDlete) {
                unlink($this->dir . $file);
            }
        }
        @file_put_contents($cache, $this->currentTime); //设置最后清理时间
        return true;
    }
}

使用:

该类仅向外提供一个公共方法:

isLimit()

返回布尔值,当为true时表示已被限制,不能继续操作

 

 

 

本书共161小节。


目前全部收费内容共295.00元。购买全部

评论 (0)