共用方式為


使用動態IP限制建立FTP驗證提供者

羅伯特·麥克默里

Microsoft 已建立已完全重寫 Windows Server® 2008 的新 FTP 服務。 這個新的 FTP 服務包含許多新功能,可讓 Web 作者比以往更輕鬆地發佈內容,並提供 Web 系統管理員更多的安全性和部署選項。

新的 FTP 7.5 服務支援擴充性,可讓您擴充 FTP 服務隨附的內建功能。 更具體來說,FTP 7.5 支援建立您自己的驗證提供者。 您也可以建立自定義 FTP 記錄的提供者,以及判斷 FTP 使用者的主目錄資訊。

本逐步解說將引導您完成將 Managed 程式代碼用於 FTP 驗證提供者的步驟,以提供使用 SQL Server 資料庫來儲存帳戶資訊的動態 IP 限制支援。 此提供者會藉由記錄遠端IP位址的失敗數目,然後使用這項資訊來封鎖無法在指定時間範圍內登入伺服器的IP位址,以實作此邏輯。

重要

必須安裝最新版本的 FTP 7.5 服務,才能在本逐步解說中使用提供者。 FTP 7.5 版本於 2009 年 8 月 3 日發行,解決了 IFtpLogProvider.Log() 方法中本機和遠端 IP 位址不正確的問題。 因此,使用舊版 FTP 服務可防止此提供者運作。

必要條件

需要下列專案才能完成本文中的程式:

  1. IIS 7.0 或更新版本必須安裝在 Windows Server 2008 伺服器上,而且也必須安裝 網際網路資訊服務 (IIS) 管理員。

  2. 必須安裝新的 FTP 7.5 服務。

    重要

    如本逐步解說稍早所述,必須安裝最新版的 FTP 7.5 服務,才能在此逐步解說中使用提供者。 FTP 7.5 版本於 2009 年 8 月 3 日發行,解決了 IFtpLogProvider.Log() 方法中本機和遠端 IP 位址不正確的問題。 因此,使用舊版 FTP 服務可防止此提供者運作。

  3. 您必須為網站啟用 FTP 發佈功能。

  4. 您必須使用 Visual Studio 2008。

    注意

    如果您使用舊版 Visual Studio,本逐步解說中的某些步驟可能不正確。

  5. 您必須針對使用者帳戶清單和相關聯的限制清單使用 SQL Server 資料庫;這個範例無法與 FTP 基本身份驗證搭配使用。 本逐步解說的「其他資訊」區段包含 SQL Server 的腳本,可建立此範例的必要數據表。

  6. 您必須在 IIS 計算機上Gacutil.exe;這是將元件新增至全域程式集緩存 (GAC) 的必要專案。

重要

為了協助改善驗證要求的效能,FTP 服務預設會快取認證以成功登入 15 分鐘。 此驗證提供者會立即拒絕攻擊者的要求,但如果攻擊者能夠成功猜測最近登入的使用者密碼,他們可能會透過快取的認證取得存取權。 這可能會導致惡意使用者在此提供者封鎖其IP位址之後攻擊您的伺服器。 若要緩解這種潛在的攻擊途徑,您應該停用 FTP 服務的認證快取。 若要這樣做,請使用下列步驟:

  1. 開啟命令提示字元。

  2. 輸入下列命令:

    cd /d "%SystemRoot%\System32\Inetsrv"
    Appcmd.exe set config -section:system.ftpServer/caching /credentialsCache.enabled:"False" /commit:apphost
    Net stop FTPSVC
    Net start FTPSVC
    
  3. 關閉命令提示字元。

在您進行這些變更之後,此範例中的驗證提供者就能夠立即拒絕來自潛在攻擊者的所有要求。

提供者描述

本逐步解說包含數個需要討論的要點。 以因特網為基礎的攻擊通常會惡意探索 FTP 伺服器,以嘗試取得系統上帳戶的使用者名稱和密碼。 偵測此行為可透過分析 FTP 活動記錄,並檢查用來攻擊您系統的 IP 位址,並封鎖這些地址無法未來存取。 不幸的是,這是手動程式,即使該程式是自動化的,也不會是即時的。

FTP 服務包含一項功能,可用來根據IP位址限制連線,但IP位址清單會儲存在IIS組態檔中,而且需要系統管理存取權才能更新。 FTP 服務的擴充性程式會以沒有許可權的較低許可權帳戶執行,以更新 IIS 組態檔的必要設定。 您可以撰寫 FTP 記錄提供者,以偵測使用者名稱泛濫,並將該資訊寫入數據存放區,以及以較高許可權帳戶執行的個別服務,以更新 IIS 組態檔,但需要更了解系統架構,而且需要一些困難的實作詳細數據。 因此,必須有替代的數據存放區。

資料庫是理想的選擇,因為數據存取容易,而且工具已正式推出,可用來操作資料庫中的數據。 下一個挑戰是使用現有的 FTP 擴充性介面來實作必要的邏輯,以偵測攻擊者將使用的登入洪水。 透過檢閱,可用的擴充性介面如下:

您可以輕鬆地撰寫利用所有這些介面的提供者,以將安全性加強到更高程度,但本逐步解說中的提供者只會使用下列介面:

  • IFtpAuthenticationProvider - 提供者會使用此介面來允許或拒絕 FTP 伺服器的存取。
  • IFtpLogProvider - 提供者會使用此介面作為泛型事件接聽程式。

FTP 服務沒有提供者可以註冊的實際事件通知,但您可以撰寫使用 IFtpLogProvider.Log() 方法來提供事件後處理的提供者。 例如,任何失敗的登入嘗試都會使用 「230」 以外的狀態代碼來記錄 「PASS」 命令,這是成功 FTP 登入的狀態代碼。 藉由擷取失敗登入嘗試的其他資訊,例如無法登入之用戶端的IP位址,就可以使用這項資訊來提供其他功能,例如封鎖IP位址在未來存取FTP伺服器。

提供者架構和邏輯

下列描述摘要說明此驗證提供者的行為:

  • 當您在系統上註冊提供者時,您會指定要使用的資料庫連線,以及 IIS 組態檔中失敗登入嘗試次數和洪水逾時的值。

  • 當 FTP 服務載入您的提供者時,它會將 IIS 組態檔中的值提供給提供者的 Initialize() 方法。 將這些值儲存在全域設定中之後, Initialize() 方法會執行一些初始垃圾收集,以清除可能位於資料庫中先前 FTP 會話的任何資訊。

  • 當 FTP 用戶端連線到 FTP 伺服器時,提供者的 Log() 方法會由 FTP 服務傳送「ControlChannelOpened」訊息。 Log() 方法會檢查資料庫,以查看用戶端的 IP 位址是否已遭到封鎖;如果是,則會將資料庫中的會話標幟。

  • 當使用者輸入其使用者名稱和密碼時,FTP 服務會呼叫提供者的 AuthenticateUser() 方法,以檢查會話是否已標幟。 如果已標示會話,提供者會 傳回 false,表示用戶無法登入。 如果未標示會話,則會使用資料庫檢查用戶名稱和密碼,以查看它們是否有效。 如果它們有效,此方法會 傳回 true,表示使用者有效且可以登入。

  • 如果使用者無法輸入有效的使用者名稱和密碼, FTP 服務會呼叫 Log() 方法,而且此方法會定期執行垃圾收集,以確保失敗數目小於洪水逾時。 接下來,方法會檢查剩餘的失敗數目是否小於失敗數目上限:

    • 如果尚未達到失敗數目上限,方法會將用戶端 IP 位址的失敗通知新增至資料庫。
    • 如果已達到失敗數目上限,方法會將用戶端的IP位址新增至資料庫中封鎖的IP位址清單。
  • 當 FTP 用戶端與伺服器中斷連線時,FTP 服務會呼叫提供者的 Log() 方法,並傳送 “ControlChannelClosed” 訊息。 Log() 方法會利用此通知來執行會話的垃圾收集。

其他注意事項

  • 此提供者會公開使用者和IP位址驗證的功能,但不會提供角色查閱的實作。 也就是說,將額外的數據表新增至使用者對角色對應,並將IFtpRoleProvider.IsUserInRole() 方法新增至提供者,但這在本逐步解說的範圍之外相當容易。
  • 此提供者會在驗證程式期間對 SQL 資料庫伺服器進行少量呼叫。 透過將一些 SQL 語句合併成單一復合查詢或預存程式,您可以進一步減少對資料庫的來回行程數目,但這不是本逐步解說的範圍。

步驟 1:設定項目環境

在此步驟中,您將為示範提供者在Visual Studio 2008中建立專案。

  1. 開啟 Microsoft Visual Studio 2008。

  2. 單擊 [ 檔案] 功能表,然後按下 [新增],然後按下 [ 專案]。

  3. 在 [新增專案] 對話方塊中:

    • 選擇 [Visual C# ] 作為項目類型。
    • 選擇 [類別庫 ] 作為範本。
    • 輸入 FtpAddressRestrictionAuthentication 作為項目的名稱。
    • 按一下 [確定]
  4. 當項目開啟時,將參考路徑新增至 FTP 擴充性連結庫:

    • 按兩下 [專案],然後按兩下 [ FtpAddressRestrictionAuthentication 屬性]。

    • 按兩下 [ 參考路徑] 索引標籤

    • 輸入WINDOWS版本的 FTP 擴充性元件路徑,其中 C: 是作業系統磁碟驅動器。

      • 針對 Windows Server 2008 和 Windows Vista:

        C:\Windows\assembly\GAC_MSIL\Microsoft.Web.FtpServer\7.5.0.0__31bf3856ad364e35
        
      • 若為 Windows 7:

        C:\Program Files\Reference Assemblies\Microsoft\IIS
        
    • 按兩下 [ 新增資料夾]。

  5. 將強名稱索引鍵新增至專案:

    • 按兩下 [專案],然後按兩下 [ FtpAddressRestrictionAuthentication 屬性]。
    • 按一下 [ 簽署 ] 索引標籤。
    • 核取 [ 簽署元件 ] 複選框。
    • 從強式索引鍵名稱下拉式方塊中選擇 [新增...>]。<
    • 輸入 FtpAddressRestrictionAuthenticationKey 以取得密鑰檔名。
    • 如有需要,請輸入金鑰檔案的密碼;否則,請清除 [ 使用密碼 保護我的金鑰檔案] 複選框。
    • 按一下 [確定]
  6. 選擇性:您可以新增自訂建置事件,將 DLL 自動新增至開發電腦上的全域程式集快取 (GAC):

    • 按兩下 [專案],然後按兩下 [ FtpAddressRestrictionAuthentication 屬性]。

    • 按兩下 [ 建置事件] 索引 標籤。

    • [建置後事件命令行 ] 對話框中輸入下列內容:

      net stop ftpsvc
      call "%VS90COMNTOOLS%\vsvars32.bat">null
      gacutil.exe /if "$(TargetPath)"
      net start ftpsvc
      
  7. 儲存專案。

步驟 2:建立擴充性類別

在此步驟中,您將實作示範提供者的記錄擴充性介面。

  1. 為專案新增 FTP 擴充性連結庫的參考:

    • 按兩下 [專案],然後按兩下 [ 新增參考...
    • 在 [.NET] 索引卷標上,按兩下 [Microsoft.Web.FtpServer]。
    • 按一下 [確定]
  2. 為專案新增 System.Web 的參考:

    • 按兩下 [專案],然後按兩下 [ 新增參考...
    • 在 [.NET] 索引卷標上,按兩下 [System.Web]。
    • 按一下 [確定]
  3. 為專案新增 System.Configuration 的參考:

    • 按兩下 [專案],然後按兩下 [ 新增參考...
    • 在 [.NET] 索引卷標上,按兩下 [System.Configuration]。
    • 按一下 [確定]
  4. 為專案新增 System.Data 的參考:

    • 按兩下 [專案],然後按兩下 [ 新增參考]。
    • 在 [.NET] 索引卷標上,按兩下 [System.Data]。
    • 按一下 [確定]
  5. 新增驗證類別的程式代碼:

    • 方案總管 中,按兩下Class1.cs檔案。

    • 拿掉現有的程序代碼。

    • 將下列程式碼貼到編輯器中:

      using System;
      using System.Collections.Generic;
      using System.Collections.Specialized;
      using System.Configuration.Provider;
      using System.Data;
      using System.Data.SqlClient;
      using System.Text;
      using Microsoft.Web.FtpServer;
      
      public class FtpAddressRestrictionAuthentication :
        BaseProvider,
        IFtpLogProvider,
        IFtpAuthenticationProvider
      {
        // Define the default values - these are only
        // used if the configuration settings are not set.
        const int defaultLogonAttempts = 5;
        const int defaultFloodSeconds = 30;
      
        // Define a connection string with no default.
        private static string _connectionString;
      
        // Initialize the private variables with the default values.
        private static int _logonAttempts = defaultLogonAttempts;
        private static int _floodSeconds = defaultFloodSeconds;
      
        // Flag the application as uninitialized.
        private static bool _initialized = false;
      
        // Define a list that will contain the list of flagged sessions.
        private static List<string> _flaggedSessions;
      
        // Initialize the provider.
        protected override void Initialize(StringDictionary config)
        {
          // Test if the application has already been initialized.
          if (_initialized == false)
          {
            // Create the flagged sessions list.
            _flaggedSessions = new List<string>();
      
            // Retrieve the connection string for the database connection.
            _connectionString = config["connectionString"];
            if (string.IsNullOrEmpty(_connectionString))
            {
              // Raise an exception if the connection string is missing or empty.
              throw new ArgumentException(
                "Missing connectionString value in configuration.");
            }
            else
            {
              // Determine whether the database is a Microsoft Access database.
              if (_connectionString.Contains("Microsoft.Jet"))
              {
                // Throw an exception if the database is a Microsoft Access database.
                throw new ProviderException("Microsoft Access databases are not supported.");
              }
            }
      
            // Retrieve the number of failures before an IP
            // address is locked out - or use the default value.
            if (int.TryParse(config["logonAttempts"], out _logonAttempts) == false)
            {
              // Set to the default if the number of logon attempts is not valid.
              _logonAttempts = defaultLogonAttempts;
            }
      
            // Retrieve the number of seconds for flood
            // prevention - or use the default value.
            if (int.TryParse(config["floodSeconds"], out _floodSeconds) == false)
            {
              // Set to the default if the number of logon attempts is not valid.
              _floodSeconds = defaultFloodSeconds;
            }
      
            // Test if the number is a positive integer and less than 10 minutes.
            if ((_floodSeconds <= 0) || (_floodSeconds > 600))
            {
              // Set to the default if the number of logon attempts is not valid.
              _floodSeconds = defaultFloodSeconds;
            }
      
            // Initial garbage collection.
            GarbageCollection(true);
            // Flag the provider as initialized.
            _initialized = true;
          }
        }
      
        // Dispose of the provider.
        protected override void Dispose(bool disposing)
        {
          base.Dispose(disposing);
      
          // Test if the application has already been uninitialized.
          if (_initialized == true)
          {
            // Final garbage collection.
            GarbageCollection(true);
            // Flag the provider as uninitialized.
            _initialized = false;
          }
        }
      
        // Authenticate a user.
        bool IFtpAuthenticationProvider.AuthenticateUser(
          string sessionId,
          string siteName,
          string userName,
          string userPassword,
          out string canonicalUserName)
        {
          // Define the canonical user name.
          canonicalUserName = userName;
      
          // Check if the session is flagged.
          if (IsSessionFlagged(sessionId) == true)
          {
            // Return false (authentication failed) if the session is flagged.
            return false;
          }
      
          // Check the user credentials and return the status.
          return IsValidUser(userName, userPassword);
        }
      
        // Implement custom actions by using the Log() method.
        void IFtpLogProvider.Log(FtpLogEntry loggingParameters)
        {
          // Test if the control channel was opened or the USER command was sent.
          if ((String.Compare(loggingParameters.Command,
            "ControlChannelOpened", true) == 0)
            || (String.Compare(loggingParameters.Command,
            "USER", true) == 0))
          {
            // Check if the IP address is banned.
            if (IsAddressBanned(loggingParameters.RemoteIPAddress) == true)
            {
              // If the IP is banned, flag the session.
              FlagSession(loggingParameters.SessionId);
              return;
            }
          }
          // Test if the PASS command was sent.
          if (String.Compare(loggingParameters.Command,
            "PASS", true) == 0)
          {
            // Check for password failures (230 is a success).
            if (loggingParameters.FtpStatus != 230)
            {
              // Periodic garbage collection - remove authentication
              // failures that are older than the flood timeout.
              GarbageCollection(false);
      
              // Test if the existing number of failures exceeds the maximum logon attempts.
              if (GetRecordCountByCriteria("[Failures]",
                "[IPAddress]='" + loggingParameters.RemoteIPAddress +
                "'") < _logonAttempts)
              {
                // Add the failure to the list of failures.
                InsertDataIntoTable("[Failures]",
                  "[IPAddress],[FailureDateTime]",
                  "'" + loggingParameters.RemoteIPAddress +
                  "','" + DateTime.Now.ToString() + "'");
              }
              else
              {
                // Ban the IP address if authentication has failed
                // from that IP more than the defined number of failures.
                BanAddress(loggingParameters.RemoteIPAddress);
                FlagSession(loggingParameters.SessionId);
              }
              return;
            }
          }
          // Test if the control channel was closed.
          if (String.Compare(loggingParameters.Command,
            "ControlChannelClosed", true) == 0)
          {
            // Session-based garbage collection - remove the
            // current session from the list of flagged sessions.
            _flaggedSessions.Remove(loggingParameters.SessionId);
            return;
          }
        }
      
        // Check for a valid username/password.
        private static bool IsValidUser(
          string userName,
          string userPassword)
        {
          // Define the initial status as the credentials are not valid.
          try
          {
            // Create a new SQL connection object.
            using (SqlConnection connection = new SqlConnection(_connectionString))
            {
              // Create a new SQL command object.
              using (SqlCommand command = new SqlCommand())
              {
                // Specify the connection for the command object.
                command.Connection = connection;
                // Specify a text command type.
                command.CommandType = CommandType.Text;
      
                // Specify the SQL text for the command object.
                command.CommandText = "SELECT COUNT(*) AS [NumRecords] " +
                  "FROM [Users] WHERE [UID]=@UID AND [PWD]=@PWD AND [Locked]=0";
      
                // Add parameters for the user name and password.
                command.Parameters.Add("@UID", SqlDbType.NVarChar).Value = userName;
                command.Parameters.Add("@PWD", SqlDbType.NVarChar).Value = userPassword;
      
                // Open the database connection.
                connection.Open();
                // Return the valid status for the credentials.
                return ((int)command.ExecuteScalar() > 0);
              }
            }
          }
          catch (Exception ex)
          {
            // Raise an exception if an error occurs.
            throw new ProviderException(ex.Message);
          }
        }
      
        // Check if the IP is banned.
        private bool IsAddressBanned(string ipAddress)
        {
          // Return whether the IP address was found in the banned addresses table.
          return (GetRecordCountByCriteria("[BannedAddresses]",
            "[IPAddress]='" + ipAddress + "'") != 0);
        }
      
        // Check if the session is flagged.
        private bool IsSessionFlagged(string sessionId)
        {
          // Return whether the session ID was found in the flagged sessions table.
          return _flaggedSessions.Contains(sessionId);
        }
      
        // Mark a session as flagged.
        private void FlagSession(string sessionId)
        {
          // Check if the session is already flagged.
          if (IsSessionFlagged(sessionId) == false)
          {
            // Flag the session if it is not already flagged.
            _flaggedSessions.Add(sessionId);
          }
        }
      
        // Mark an IP address as banned.
        private void BanAddress(string ipAddress)
        {
          // Check if the IP address is already banned.
          if (IsAddressBanned(ipAddress) == false)
          {
            // Ban the IP address if it is not already banned.
            InsertDataIntoTable("[BannedAddresses]",
              "[IPAddress]", "'" + ipAddress + "'");
          }
        }
      
        // Perform garbage collection tasks.
        private void GarbageCollection(bool deleteSessions)
        {
          // Remove any authentication failures that are older than the flood timeout.
          DeleteRecordsByCriteria("[Failures]",
            String.Format("DATEDIFF(second,[FailureDateTime],'{0}')>{1}",
            DateTime.Now.ToString(),_floodSeconds.ToString()));
      
          // Test if flagged sessions should be deleted.
          if (deleteSessions == true)
          {
            // Remove any sessions from the list of flagged sessions.
            _flaggedSessions.Clear();
          }
        }
      
        // Retrieve the count of records based on definable criteria.
        private int GetRecordCountByCriteria(
          string tableName,
          string criteria)
        {
          // Create a SQL string to retrieve the count of records 
          // that are found in a table based on the criteria.
          StringBuilder sqlString = new StringBuilder();
          sqlString.Append("SELECT COUNT(*) AS [NumRecords]");
          sqlString.Append(String.Format(
            " FROM {0}",tableName));
          sqlString.Append(String.Format(
            " WHERE {0}",criteria));
          // Execute the query.
          return ExecuteQuery(true, sqlString.ToString());
        }
      
        // Insert records into a database table.
        private void InsertDataIntoTable(
          string tableName,
          string fieldNames,
          string fieldValues)
        {
          // Create a SQL string to insert data into a table.
          StringBuilder sqlString = new StringBuilder();
          sqlString.Append(String.Format(
            "INSERT INTO {0}",tableName));
          sqlString.Append(String.Format(
            "({0}) VALUES({1})",fieldNames, fieldValues));
          // Execute the query.
          ExecuteQuery(false, sqlString.ToString());
        }
      
        // Remove records from a table based on criteria.
        private void DeleteRecordsByCriteria(
          string tableName,
          string queryCriteria)
        {
          // Create a SQL string to delete data from a table.
          StringBuilder sqlString = new StringBuilder();
          sqlString.Append(String.Format(
            "DELETE FROM {0}",tableName));
          // Test if any criteria is specified.
          if (string.IsNullOrEmpty(queryCriteria) == false)
          {
            // Append the criteria to the SQL string.
            sqlString.Append(String.Format(
              " WHERE {0}",queryCriteria));
          }
          // Execute the query.
          ExecuteQuery(false, sqlString.ToString());
        }
      
        // Execute SQL queries.
        private int ExecuteQuery(bool returnRecordCount, string sqlQuery)
        {
          try
          {
            // Create a new SQL connection object.
            using (SqlConnection connection =
              new SqlConnection(_connectionString))
            {
              // Create a new SQL command object.
              using (SqlCommand command =
                new SqlCommand(sqlQuery, connection))
              {
                // Open the connection.
                connection.Open();
                // Test whether the method should return a record count.
                if (returnRecordCount == true)
                {
                  // Run the database query.
                  SqlDataReader dataReader = command.ExecuteReader();
                  // Test if data reader has returned any rows.
                  if (dataReader.HasRows)
                  {
                    // Read a single row.
                    dataReader.Read();
                    // Return the number of records.
                    return ((int)dataReader["NumRecords"]);
                  }
                }
                else
                {
                  // Run the database query.
                  command.ExecuteNonQuery();
                }
              }
            }
            // Return a zero record count.
            return 0;
          }
          catch (Exception ex)
          {
            // Raise an exception if an error occurs.
            throw new ProviderException(ex.Message);
          }
        }
      }
      
  6. 儲存並編譯專案。

注意

如果您未使用選擇性步驟在 GAC 中註冊元件,您必須手動將元件複製到 IIS 計算機,並使用 Gacutil.exe 工具將元件新增至 GAC。 如需詳細資訊,請參閱 Gacutil.exe (全域組件快取工具)

步驟 3:將示範提供者新增至 FTP

在此步驟中,您會將示範提供者新增至 FTP 服務和默認網站。

  1. 判斷擴充性提供者的元件資訊:

    • 在 Windows 檔案總管中,開啟您的 C:\Windows\assembly 路徑,其中 C: 是作業系統磁碟驅動器。
    • 找出 FtpAddressRestrictionAuthentication 元件。
    • 以滑鼠右鍵按兩下元件,然後按下 [ 屬性]。
    • 複製文化特性值;例如:中性
    • 複製版本號碼;例如:1.0.0.0
    • 複製公鑰令牌值;例如:426f626f62526f636b73
    • 按一下 [取消]
  2. 使用先前步驟中的資訊,將擴充性提供者新增至 FTP 提供者的全域清單,並設定提供者的選項:

    • 目前沒有任何使用者介面可讓您新增自訂驗證模組的屬性,因此您必須使用下列命令行:

      cd %SystemRoot%\System32\Inetsrv
      
      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"[name='FtpAddressRestrictionAuthentication',type='FtpAddressRestrictionAuthentication,FtpAddressRestrictionAuthentication,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73']" /commit:apphost
      
      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication']" /commit:apphost
      
      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication'].[key='connectionString',value='Server=localhost;Database=FtpAuthentication;User ID=FtpLogin;Password=P@ssw0rd']" /commit:apphost
      
      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication'].[key='logonAttempts',value='5']" /commit:apphost
      
      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication'].[key='floodSeconds',value='30']" /commit:apphost
      

    注意

    您在 connectionString 屬性中指定的 連接字串 必須是資料庫的有效登入。

  3. 將自訂提供者新增至網站:

    • 目前沒有任何 UI 可讓您將自訂功能新增至網站,因此您必須使用下列命令行:

      AppCmd.exe set config -section:system.applicationHost/sites /"[name='Default Web Site'].ftpServer.security.authentication.basicAuthentication.enabled:False" /commit:apphost
      
      AppCmd.exe set config -section:system.applicationHost/sites /+"[name='Default Web Site'].ftpServer.security.authentication.customAuthentication.providers.[name='FtpAddressRestrictionAuthentication',enabled='True']" /commit:apphost
      
      AppCmd set site "Default Web Site" /+ftpServer.customFeatures.providers.[name='FtpAddressRestrictionAuthentication',enabled='true'] /commit:apphost
      

    注意

    此語法會停用 FTP 基本身份驗證,而且在使用這個驗證提供者時,請務必停用基本身份驗證。 否則,當此驗證提供者封鎖攻擊者的IP位址時,攻擊者仍然可以攻擊使用基本身份驗證的帳戶。

  4. 新增驗證提供者的授權規則:

    • 按兩下主視窗中的 [ FTP 授權規則 ]。

    • 按兩下 [動作] 窗格中的 [新增允許規則...]。

    • 針對存取選項選取 [ 指定使用者 ]。

    • 輸入使用者名稱。

      注意

      使用者名稱必須輸入到這個步驟清單外部的資料庫。

    • 針對 [許可權] 選項選擇 [讀取和/或寫入]。

    • 按一下 [確定]

步驟 4:搭配 FTP 7.5 使用提供者

當 FTP 用戶端連線到 FTP 月臺時,FTP 服務會嘗試使用儲存在資料庫中的帳戶,向自定義驗證提供者驗證使用者。 如果 FTP 用戶端無法驗證,提供者將會追蹤資料庫中失敗的 IP 位址和日期/時間。 當 FTP 用戶端無法從特定 IP 位址登入,以取得在 logonAttempts 設定中指定的失敗次數,以及在 floodSeconds 設定中指定的時間範圍內,提供者會封鎖 IP 位址登入 FTP 服務。

注意

此範例提供者會實作 FTP 服務的驗證邏輯,但未提供管理模組來管理資料庫中的數據。 例如,您無法使用此提供者來管理 FTP 使用者帳戶、禁用 IP 位址或驗證失敗的清單。 若要使用 IIS 管理員管理數據,您可以使用 IIS 資料庫管理員。 如需詳細資訊,請參閱下列主題:

https://www.iis.net/extensions/DatabaseManager

其他資訊

您可以使用下列適用於 Microsoft SQL Server 的 SQL 腳本來建立必要的資料庫和資料表。 若要使用此文稿,您必須更新資料庫名稱和資料庫檔案的位置。 在 SQL Server 中,您會在新查詢視窗中執行腳本,然後建立將搭配 連接字串 使用的資料庫登入。

注意

您可能想要改變 SQL 腳本,將資料庫儲存在 以外的 c:\databases位置。

/****** Create the FtpAuthentication Database ******/

USE [master]
GO
CREATE DATABASE [FtpAuthentication] ON  PRIMARY 
( NAME = N'FtpAuthentication', FILENAME = N'c:\databases\FtpAuthentication.mdf' , SIZE = 2048KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB )
 LOG ON 
( NAME = N'FtpAuthentication_log', FILENAME = N'c:\databases\FtpAuthentication_log.ldf' , SIZE = 1024KB , MAXSIZE = 2048GB , FILEGROWTH = 10%)
 COLLATE SQL_Latin1_General_CP1_CI_AS
GO
EXEC dbo.sp_dbcmptlevel @dbname=N'FtpAuthentication', @new_cmptlevel=90
GO
IF (1 = FULLTEXTSERVICEPROPERTY('IsFullTextInstalled'))
begin
EXEC [FtpAuthentication].[dbo].[sp_fulltext_database] @action = 'enable'
end
GO
ALTER DATABASE [FtpAuthentication] SET ANSI_NULL_DEFAULT OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ANSI_NULLS OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ANSI_PADDING OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ANSI_WARNINGS OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ARITHABORT OFF 
GO
ALTER DATABASE [FtpAuthentication] SET AUTO_CLOSE OFF 
GO
ALTER DATABASE [FtpAuthentication] SET AUTO_CREATE_STATISTICS ON 
GO
ALTER DATABASE [FtpAuthentication] SET AUTO_SHRINK OFF 
GO
ALTER DATABASE [FtpAuthentication] SET AUTO_UPDATE_STATISTICS ON 
GO
ALTER DATABASE [FtpAuthentication] SET CURSOR_CLOSE_ON_COMMIT OFF 
GO
ALTER DATABASE [FtpAuthentication] SET CURSOR_DEFAULT  GLOBAL 
GO
ALTER DATABASE [FtpAuthentication] SET CONCAT_NULL_YIELDS_NULL OFF 
GO
ALTER DATABASE [FtpAuthentication] SET NUMERIC_ROUNDABORT OFF 
GO
ALTER DATABASE [FtpAuthentication] SET QUOTED_IDENTIFIER OFF 
GO
ALTER DATABASE [FtpAuthentication] SET RECURSIVE_TRIGGERS OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ENABLE_BROKER 
GO
ALTER DATABASE [FtpAuthentication] SET AUTO_UPDATE_STATISTICS_ASYNC OFF 
GO
ALTER DATABASE [FtpAuthentication] SET DATE_CORRELATION_OPTIMIZATION OFF 
GO
ALTER DATABASE [FtpAuthentication] SET TRUSTWORTHY OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ALLOW_SNAPSHOT_ISOLATION OFF 
GO
ALTER DATABASE [FtpAuthentication] SET PARAMETERIZATION SIMPLE 
GO
ALTER DATABASE [FtpAuthentication] SET READ_WRITE 
GO
ALTER DATABASE [FtpAuthentication] SET RECOVERY SIMPLE 
GO
ALTER DATABASE [FtpAuthentication] SET MULTI_USER 
GO
ALTER DATABASE [FtpAuthentication] SET PAGE_VERIFY CHECKSUM  
GO
ALTER DATABASE [FtpAuthentication] SET DB_CHAINING OFF 

/****** Create the Database Tables ******/

USE [FtpAuthentication]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[BannedAddresses]') AND type in (N'U'))
BEGIN
CREATE TABLE [BannedAddresses](
    [IPAddress] [nvarchar](50) NOT NULL
) ON [PRIMARY]
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Failures]') AND type in (N'U'))
BEGIN
CREATE TABLE [Failures](
    [IPAddress] [nvarchar](50) NOT NULL,
    [FailureDateTime] [datetime] NOT NULL
) ON [PRIMARY]
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Users]') AND type in (N'U'))
BEGIN
CREATE TABLE [Users](
    [UID] [nvarchar](50) NOT NULL,
    [PWD] [nvarchar](50) NOT NULL,
    [Locked] [bit] NOT NULL
) ON [PRIMARY]
END

摘要

在本逐步解說中,您已瞭解如何:

  • 在 Visual Studio 2008 中為自定義 FTP 提供者建立專案。
  • 實作自定義 FTP 提供者的擴充性介面。
  • 將 FTP 自定義提供者新增至 FTP 服務。