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表單提交。

相關文章
相關標籤/搜索