Drupal flood 行为溢出控制机制详解

Drupal项目中可能会时常遇到登陆时尝试次数过多被提示以下的状况:前端

Sorry, there have been more than 5 failed login attempts for this account. It is temporarily blocked. Try again later or request a new password.

就是你尝试过多的登陆该用户名的次数了,这个用户名被临时禁止登陆了,待会你再试试或者请求新密码。固然只是当前访问的IP被禁止用该用户名登陆,其它IP去登陆仍是正常的。默认对IP+用户名的限制是5次,超过了就会被禁止必定时间,除非去手动清除flood表里的记录。web

那么这个flood究竟是个什么东西呢?drupal也没给咱们提供任何后端配置页面。它除了对登陆行为作控制,还能作些什么呢?下面就给你们介绍下:ajax

Drupal提供的flood机制,通俗的理解就是:在必定的时间内,某个对象只能作多少次某个行为。我起了个中文名叫:行为溢出控制。后端

Drupal默认提供了两个行为溢出控制:user login和contact。咱们以用户登陆为例,经过搜索上面的部分error提示文字,能够定位到用户登陆行为使用flood的位置:app

function user_login_final_validate($form, &$form_state) {
  if (empty($form_state['uid'])) {
    // Always register an IP-based failed login event.
    flood_register_event('failed_login_attempt_ip', variable_get('user_failed_login_ip_window', 3600));
    // Register a per-user failed login event.
    if (isset($form_state['flood_control_user_identifier'])) {
      flood_register_event('failed_login_attempt_user', variable_get('user_failed_login_user_window', 21600), $form_state['flood_control_user_identifier']);
    }

    if (isset($form_state['flood_control_triggered'])) {
      if ($form_state['flood_control_triggered'] == 'user') {
        form_set_error('name', format_plural(variable_get('user_failed_login_user_limit', 5), 'Sorry, there has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', 'Sorry, there have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', array('@url' => url('user/password'))));
      }
      else {
        // We did not find a uid, so the limit is IP-based.
        form_set_error('name', t('Sorry, too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', array('@url' => url('user/password'))));
      }
    }
    else {
      // Use $form_state['input']['name'] here to guarantee that we send
      // exactly what the user typed in. $form_state['values']['name'] may have
      // been modified by validation handlers that ran earlier than this one.
      $query = isset($form_state['input']['name']) ? array('name' => $form_state['input']['name']) : array();
      form_set_error('name', t('Sorry, unrecognized username or password. <a href="@password">Have you forgotten your password?</a>', array('@password' => url('user/password', array('query' => $query)))));
      watchdog('user', 'Login attempt failed for %user.', array('%user' => $form_state['values']['name']));
    }
  }
  elseif (isset($form_state['flood_control_user_identifier'])) {
    // Clear past failures for this user so as not to block a user who might
    // log in and out more than once in an hour.
    flood_clear_event('failed_login_attempt_user', $form_state['flood_control_user_identifier']);
  }
}

这个函数是登陆表单提交时的最终验证步骤的调用方法, 咱们能够很明显的看到有两个跟flood有关的函数调用:flood_register_event 和 flood_clear_event。下面咱们把函数体贴出来看下:less

/**
 * Registers an event for the current visitor to the flood control mechanism.
 *
 * @param $name
 *   The name of an event.
 * @param $window
 *   Optional number of seconds before this event expires. Defaults to 3600 (1
 *   hour). Typically uses the same value as the flood_is_allowed() $window
 *   parameter. Expired events are purged on cron run to prevent the flood table
 *   from growing indefinitely.
 * @param $identifier
 *   Optional identifier (defaults to the current user's IP address).
 */
function flood_register_event($name, $window = 3600, $identifier = NULL) {
  if (!isset($identifier)) {
    $identifier = ip_address();
  }
  db_insert('flood')
    ->fields(array(
      'event' => $name,
      'identifier' => $identifier,
      'timestamp' => REQUEST_TIME,
      'expiration' => REQUEST_TIME + $window,
    ))
    ->execute();
}

flood_register_event是用于注册flood事件,该函数的第一个参数是事件名称,登陆这里用到了两个事件,1.failed_login_attempt_ip 2. failed_login_attempt_user。 意思是分别对ip和用户名的登陆行为作溢出控制。ide

第二个参数是窗口时间,能够理解为该事件类型的窗口时间,即咱们要控制多长时间内的操做频次。函数

第三个参数是对象标识,好比一般咱们控制的是ip地址的操做频率,在登陆行为这里的控制就是若是客户端IP一直在尝试登陆可是失败了必定次数,这个ip就会被禁止登陆行为一段时间,默认是50次。用户登陆这里还用到了IP+用户名的标识(能够经过修改变量:user_failed_login_identifier_uid_only的值变成对用户名的全局控制,可是这个控制方式反作用太大,因此通常不采用),就是若是客户端一直尝试某个用户名的登陆失败,该用户名+IP为组合的标识也会被封禁一段时间,意思就是在必定时间内这个ip不能再用这个用户名来尝试登陆了。fetch

还有一个函数是:flood_clear_event网站

/**
 * Makes the flood control mechanism forget an event for the current visitor.
 *
 * @param $name
 *   The name of an event.
 * @param $identifier
 *   Optional identifier (defaults to the current user's IP address).
 */
function flood_clear_event($name, $identifier = NULL) {
  if (!isset($identifier)) {
    $identifier = ip_address();
  }
  db_delete('flood')
    ->condition('event', $name)
    ->condition('identifier', $identifier)
    ->execute();
}

顾名思义,就是清除溢出记录,根据参数来看,清除的是某个标识的某个事件的溢出记录。用户登陆这里是在用户登陆成功后,将该用户名的标识的登陆事件清空,意思是你登陆成功了说明你是正经常使用户,就不给你累计异常了。

有了注册和清除,那么还缺判断,就是咱们应该须要在登陆时判断到底当前的标识对象有没有超过溢出控制频次?

经过对用户登陆过程的验证调用顺序,咱们看到,第二步登陆验证的方法:user_login_authenticate_validate,这个函数里使用了flood_is_allowed函数来实现判断。

/**
 * A validate handler on the login form. Check supplied username/password
 * against local users table. If successful, $form_state['uid']
 * is set to the matching user ID.
 */
function user_login_authenticate_validate($form, &$form_state) {
  $password = trim($form_state['values']['pass']);
  if (!empty($form_state['values']['name']) && strlen(trim($password)) > 0) {
    // Do not allow any login from the current user's IP if the limit has been
    // reached. Default is 50 failed attempts allowed in one hour. This is
    // independent of the per-user limit to catch attempts from one IP to log
    // in to many different user accounts.  We have a reasonably high limit
    // since there may be only one apparent IP for all users at an institution.
    if (!flood_is_allowed('failed_login_attempt_ip', variable_get('user_failed_login_ip_limit', 50), variable_get('user_failed_login_ip_window', 3600))) {
      $form_state['flood_control_triggered'] = 'ip';
      return;
    }
    $account = db_query("SELECT * FROM {users} WHERE name = :name AND status = 1", array(':name' => $form_state['values']['name']))->fetchObject();
    if ($account) {
      if (variable_get('user_failed_login_identifier_uid_only', FALSE)) {
        // Register flood events based on the uid only, so they apply for any
        // IP address. This is the most secure option.
        $identifier = $account->uid;
      }
      else {
        // The default identifier is a combination of uid and IP address. This
        // is less secure but more resistant to denial-of-service attacks that
        // could lock out all users with public user names.
        $identifier = $account->uid . '-' . ip_address();
      }
      $form_state['flood_control_user_identifier'] = $identifier;

      // Don't allow login if the limit for this user has been reached.
      // Default is to allow 5 failed attempts every 6 hours.
      if (!flood_is_allowed('failed_login_attempt_user', variable_get('user_failed_login_user_limit', 5), variable_get('user_failed_login_user_window', 21600), $identifier)) {
        $form_state['flood_control_triggered'] = 'user';
        return;
      }
    }
    // We are not limited by flood control, so try to authenticate.
    // Set $form_state['uid'] as a flag for user_login_final_validate().
    $form_state['uid'] = user_authenticate($form_state['values']['name'], $password);
  }
}
/**
 * Checks whether a user is allowed to proceed with the specified event.
 *
 * Events can have thresholds saying that each user can only do that event
 * a certain number of times in a time window. This function verifies that the
 * current user has not exceeded this threshold.
 *
 * @param $name
 *   The unique name of the event.
 * @param $threshold
 *   The maximum number of times each user can do this event per time window.
 * @param $window
 *   Number of seconds in the time window for this event (default is 3600
 *   seconds, or 1 hour).
 * @param $identifier
 *   Unique identifier of the current user. Defaults to their IP address.
 *
 * @return
 *   TRUE if the user is allowed to proceed. FALSE if they have exceeded the
 *   threshold and should not be allowed to proceed.
 */
function flood_is_allowed($name, $threshold, $window = 3600, $identifier = NULL) {
  if (!isset($identifier)) {
    $identifier = ip_address();
  }
  $number = db_query("SELECT COUNT(*) FROM {flood} WHERE event = :event AND identifier = :identifier AND timestamp > :timestamp", array(
    ':event' => $name,
    ':identifier' => $identifier,
    ':timestamp' => REQUEST_TIME - $window))
    ->fetchField();
  return ($number < $threshold);
}

主要看下第二个参数 threshold,以前没有出现过,这个单词译为:阈值。顾名思义就是某个标识在某个事件的窗口时间内可进行该事件行为的最大频次。

像窗口时间和阈值,每一个事件有是有单独的变量控制的,好比用户登陆的窗口时间(以ip标识为例):variable_get('user_failed_login_ip_window', 3600),阈值:variable_get('user_failed_login_ip_limit', 50),含义就是:在1小时内,每一个ip只能进行50次登陆行为。超过这个阈值就会被封禁。

那么封禁多长时间呢?这个不是固定的时间,你们读一下flood_is_allowed函数体就了解了,就是去看下在当前时间往前的窗口时间内,累计的溢出记录的数量有没有达到阈值,若达到了就阻止接下来的行为,若没有达到,就经过,继续往下执行。

那至于你啥时候被解禁,还得看你操做的时间分布,不过假如是一口气操做的,那基本上就是在一个窗口时间后才能继续尝试。好比ip登陆默认就是在1小时后了。

因此说总结下用户登陆这里的溢出控制流程就是(以ip标识为例): 在登陆验证的流程里,先使用flood_is_allowed来判断当前ip在过去1小时内的登陆行为是否累计达到了50次,若已达到则禁止登陆,若未达到,则使用flood_register_event新增一条溢出记录,而后放行。


Druapl的这个功能隐藏的还挺深,没有被深刻挖掘出来,在官方网站搜索注意到有两个第三方模块提供了登陆和contact的窗口时间和频次的配置,以及溢出记录的清除管理。flood_unblock模块 和 flood_control模块.

image

image

那么经过对flood的深刻了解,咱们应该能够想到能够在其它地方也能用到它,好比咱们对常见的ip攻击位置作一下封禁机制,好比想对注册作一下防范,就能够设置好比1小时内一个ip只能注册10个用户。还能够对短信验证码发送作判断,好比1小时内一个ip只能发送10次验证码。

咱们能够参考flood_unblock和flood_control,本身作一个后端配置各类事件的窗口时间和阈值,还有管理flood记录的页面,能够取消封禁。

总结一下:flood机制主要是用来对ip或者用户进行敏感行为的频次控制。

扩展延伸:drupal提供的flood机制有一个明显的问题就是配置扩展不方便,新增一个事件的控制,必须是到该事件相应的关键函数里去调用,这也是为啥没有可以提供后端配置管理功能的缘由,无法在后台统一管理。而若是能改为对url统一进行判断就很方便了,跟web应用防火墙提供的配置很像,好比阿里云应用防火墙提供的cc攻击防范规则自定义功能,可是前提是你的行为是经过接口方式提供的,好比登陆注册都是前端调用你提供的ajax url,而不是直接用的drupal表单提交。

相关文章
相关标签/搜索