Condividi tramite


Creare un provider di autenticazione FTP con restrizioni IP dinamiche

di Robert McMurray

Microsoft ha creato un nuovo servizio FTP completamente riscritto per Windows Server® 2008. Questo nuovo servizio FTP incorpora molte nuove funzionalità che consentono agli autori Web di pubblicare il contenuto più facilmente rispetto a prima e offre agli amministratori Web più opzioni di sicurezza e distribuzione.

Il nuovo servizio FTP 7.5 supporta l'estendibilità che consente di estendere la funzionalità predefinita inclusa nel servizio FTP. In particolare, FTP 7.5 supporta la creazione di provider di autenticazione personalizzati. È anche possibile creare provider per la registrazione FTP personalizzata e determinare le informazioni della home directory per gli utenti FTP.

Questa procedura dettagliata illustra la procedura per usare il codice gestito a un provider di autenticazione FTP che fornisce il supporto per restrizioni IP dinamiche che usano un database di SQL Server per archiviare le informazioni sull'account. Questo provider implementa questa logica registrando il numero di errori dagli indirizzi IP remoti e quindi usando queste informazioni per bloccare gli indirizzi IP che non riescono ad accedere al server entro un determinato intervallo di tempo.

Importante

Per usare il provider in questa procedura dettagliata, è necessario installare la versione più recente del servizio FTP 7.5. Una versione FTP 7.5 è stata rilasciata il 3 agosto 2009 che ha risolto un problema per cui gli indirizzi IP locali e remoti nel metodo IFtpLogProvider.Log() non erano corretti. Per questo motivo, l'uso di una versione precedente del servizio FTP impedirà al provider di funzionare.

Prerequisiti

Per completare le procedure descritte in questo articolo sono necessari gli elementi seguenti:

  1. IIS 7.0 o versione successiva deve essere installato nel server Windows Server 2008 e deve essere installato anche Gestione Internet Information Services (IIS).

  2. È necessario installare il nuovo servizio FTP 7.5.

    Importante

    Come accennato in precedenza in questa procedura dettagliata, è necessario installare la versione più recente del servizio FTP 7.5 per usare il provider in questa procedura dettagliata. Una versione FTP 7.5 è stata rilasciata il 3 agosto 2009 che ha risolto un problema per cui gli indirizzi IP locali e remoti nel metodo IFtpLogProvider.Log() non erano corretti. Per questo motivo, l'uso di una versione precedente del servizio FTP impedirà al provider di funzionare.

  3. È necessario che la pubblicazione FTP sia abilitata per un sito.

  4. È necessario usare Visual Studio 2008.

    Nota

    Se si usa una versione precedente di Visual Studio, alcuni dei passaggi di questa procedura dettagliata potrebbero non essere corretti.

  5. È necessario usare un database di SQL Server per l'elenco di account utente e gli elenchi di restrizioni associati; Questo esempio non può essere usato con l'autenticazione FTP Basic. La sezione "Informazioni aggiuntive" di questa procedura dettagliata contiene uno script per SQL Server che crea le tabelle necessarie per questo esempio.

  6. Sarà necessario Gacutil.exe nel computer IIS; è necessario per aggiungere gli assembly alla Global Assembly Cache (GAC).

Importante

Per migliorare le prestazioni per le richieste di autenticazione, il servizio FTP memorizza nella cache le credenziali per gli accessi riusciti per 15 minuti per impostazione predefinita. Questo provider di autenticazione negherà immediatamente le richieste da un utente malintenzionato, ma se l'utente malintenzionato è riuscito a indovinare correttamente la password per un utente che ha effettuato l'accesso di recente, potrebbe ottenere l'accesso tramite le credenziali memorizzate nella cache. Ciò potrebbe avere la conseguenza involontaria di consentire a un utente malintenzionato di attaccare il server dopo che questo provider ha bloccato il proprio indirizzo IP. Per alleviare questo potenziale rischio di attacco, è necessario disabilitare la memorizzazione nella cache delle credenziali per il servizio FTP. A tale scopo, seguire questa procedura:

  1. Apri un prompt dei comandi.

  2. Digitare i comandi seguenti:

    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. Chiudere il prompt dei comandi.

Dopo aver apportato queste modifiche, il provider di autenticazione in questo esempio sarà in grado di negare immediatamente tutte le richieste da un potenziale utente malintenzionato.

Descrizione provider

Questa procedura dettagliata contiene diversi punti che richiedono alcune discussioni. Gli attacchi basati su Internet spesso sfruttano un server FTP nel tentativo di ottenere il nome utente e la password per un account in un sistema. Il rilevamento di questo comportamento è possibile tramite l'analisi dei log attività FTP e l'analisi degli indirizzi IP usati per attaccare il sistema e bloccare tali indirizzi dall'accesso futuro. Sfortunatamente, si tratta di un processo manuale e, anche se tale processo è automatizzato, non sarà in tempo reale.

Il servizio FTP contiene una funzionalità per limitare le connessioni in base agli indirizzi IP, ma l'elenco degli indirizzi IP viene archiviato nei file di configurazione IIS e richiede l'accesso amministrativo per l'aggiornamento. Il processo di estendibilità per il servizio FTP viene eseguito come account con privilegi inferiori che non dispone delle autorizzazioni per aggiornare le impostazioni necessarie i file di configurazione IIS. È possibile scrivere un provider di registrazione FTP che rileva l'inondazione del nome utente e scrive tali informazioni in un archivio dati e un servizio separato che viene eseguito come account con privilegi più elevati che può aggiornare i file di configurazione IIS, ma che richiede una conoscenza più approfondita dell'architettura di sistema e richiede una serie di dettagli di implementazione difficili. Per questo motivo, è necessario un archivio dati alternativo.

Un database offre una scelta ideale a causa della facilità di accesso ai dati e della disponibilità generale degli strumenti disponibili per la modifica dei dati in un database. La sfida successiva consiste nell'usare le interfacce di estendibilità FTP esistenti per implementare la logica necessaria per rilevare l'inondazione dell'accesso che un utente malintenzionato userebbe. Per esaminare, le interfacce di estendibilità disponibili sono:

È possibile scrivere facilmente un provider che sfrutta tutte queste interfacce per rafforzare la sicurezza in modo più elevato, ma il provider in questa procedura dettagliata userà solo le interfacce seguenti:

  • IFtpAuthenticationProvider : il provider userà questa interfaccia per consentire o negare l'accesso al server FTP.
  • IFtpLogProvider : il provider userà questa interfaccia come listener di eventi generici.

Il servizio FTP non dispone di notifiche di eventi effettive per cui un provider può registrarsi, ma è possibile scrivere provider che usano il metodo IFtpLogProvider.Log() per fornire l'elaborazione post-evento. Ad esempio, eventuali tentativi di accesso non riusciti registrano il comando "PASS" con un codice di stato diverso da "230", ovvero il codice di stato per un accesso FTP riuscito. Acquisendo informazioni aggiuntive sul tentativo di accesso non riuscito, ad esempio l'indirizzo IP del client che non è riuscito ad accedere, è possibile usare queste informazioni per fornire funzionalità aggiuntive, ad esempio impedire agli indirizzi IP di accedere al server FTP in futuro.

Architettura e logica del provider

Le descrizioni seguenti riepilogano il comportamento per questo provider di autenticazione:

  • Quando si registra il provider nel sistema, specificare la connessione al database da usare e i valori per il numero di tentativi di accesso non riusciti e il timeout di flood nei file di configurazione iis.

  • Quando il servizio FTP carica il provider, fornisce i valori dei file di configurazione iis al metodo Initialize() del provider. Dopo l'archiviazione di questi valori nelle impostazioni globali, il metodo Initialize() esegue un'operazione iniziale di Garbage Collection per pulire tutte le informazioni delle sessioni FTP precedenti che potrebbero trovarsi nel database.

  • Quando un client FTP si connette al server FTP, il metodo Log() del provider viene inviato il messaggio "ControlChannelOpened" dal servizio FTP. Il metodo Log() controlla il database per verificare se l'indirizzo IP del client è stato bloccato. In tal caso, contrassegna la sessione nel database.

  • Quando l'utente immette il nome utente e la password, il servizio FTP chiama il metodo AuthenticateUser() del provider, che verifica se la sessione è contrassegnata. Se la sessione è contrassegnata, il provider restituisce false, che indica che l'utente non è riuscito ad accedere. Se la sessione non è contrassegnata, il nome utente e la password vengono controllati con il database per verificare se sono validi. Se sono validi, il metodo restituisce true, a indicare che l'utente è valido e può eseguire l'accesso.

  • Se l'utente non riesce a immettere un nome utente e una password validi, il metodo Log() viene chiamato dal servizio FTP e il metodo esegue un'operazione di Garbage Collection periodica per assicurarsi che il numero di errori sia inferiore al timeout di flood. Il metodo verifica quindi se il numero rimanente di errori è minore del numero massimo di errori:

    • Se il numero massimo di errori non è stato raggiunto, il metodo aggiunge una notifica di errore per l'indirizzo IP del client al database.
    • Se è stato raggiunto il numero massimo di errori, il metodo aggiunge l'indirizzo IP del client all'elenco di indirizzi IP bloccati nel database.
  • Quando un client FTP si disconnette dal server, il servizio FTP chiama il metodo Log() del provider e invia il messaggio "ControlChannelClosed". Il metodo Log() sfrutta questa notifica per eseguire l'operazione di Garbage Collection per la sessione.

Note aggiuntive

  • Questo provider espone la funzionalità per la convalida degli indirizzi IP e degli utenti, ma non fornisce un'implementazione per le ricerche dei ruoli. Detto questo, sarebbe relativamente facile aggiungere una tabella aggiuntiva per i mapping da utente a ruolo e aggiungere il metodo IFtpRoleProvider.IsUserInRole() al provider, ma non rientra nell'ambito di questa procedura dettagliata.
  • Questo provider effettua un numero ridotto di chiamate al server di database SQL durante il processo di autenticazione. Tramite il consolidamento di alcune istruzioni SQL in singole query composte o stored procedure, è possibile ridurre ulteriormente il numero di round trip al database, ma non rientra nell'ambito di questa procedura dettagliata.

Passaggio 1: Configurare l'ambiente del progetto

In questo passaggio verrà creato un progetto in Visual Studio 2008 per il provider demo.

  1. Aprire Microsoft Visual Studio 2008.

  2. Fare clic sul menu File , quindi su Nuovo, quindi su Progetto.

  3. Nella finestra di dialogo Nuovo progetto:

    • Scegliere Visual C# come tipo di progetto.
    • Scegliere Libreria di classi come modello.
    • Digitare FtpAddressRestrictionAuthentication come nome del progetto.
    • Fare clic su OK.
  4. Quando si apre il progetto, aggiungere un percorso di riferimento alla libreria di estendibilità FTP:

    • Fare clic su Progetto e quindi su FtpAddressRestrictionAuthentication Properties.Click Project, and then click FtpAddressRestrictionAuthentication Properties.

    • Fare clic sulla scheda Percorsi di riferimento.

    • Immettere il percorso dell'assembly di estendibilità FTP per la versione di Windows, dove C: è l'unità del sistema operativo.

      • Per Windows Server 2008 e Windows Vista:

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

        C:\Program Files\Reference Assemblies\Microsoft\IIS
        
    • Fare clic su Aggiungi cartella.

  5. Aggiungere una chiave con nome sicuro al progetto:

    • Fare clic su Progetto e quindi su FtpAddressRestrictionAuthentication Properties.Click Project, and then click FtpAddressRestrictionAuthentication Properties.
    • Fare clic sulla scheda Firma .
    • Selezionare la casella di controllo Firma assembly .
    • Scegliere <Nuovo...> dalla casella di riepilogo a discesa Nome chiave complessa.
    • Immettere FtpAddressRestrictionAuthenticationKey per il nome del file di chiave.
    • Se lo si desidera, immettere una password per il file di chiave; in caso contrario, deselezionare la casella di controllo Proteggi il file di chiave con una password .
    • Fare clic su OK.
  6. Facoltativo: è possibile aggiungere un evento di compilazione personalizzato per aggiungere automaticamente la DLL alla Global Assembly Cache (GAC) nel computer di sviluppo:

    • Fare clic su Progetto e quindi su FtpAddressRestrictionAuthentication Properties.Click Project, and then click FtpAddressRestrictionAuthentication Properties.

    • Fare clic sulla scheda Eventi di compilazione.

    • Immettere quanto segue nella finestra di dialogo Riga di comando dell'evento post-compilazione:

      net stop ftpsvc
      call "%VS90COMNTOOLS%\vsvars32.bat">null
      gacutil.exe /if "$(TargetPath)"
      net start ftpsvc
      
  7. Salvare il progetto.

Passaggio 2: Creare la classe Extensibility

In questo passaggio si implementerà l'interfaccia di estendibilità della registrazione per il provider demo.

  1. Aggiungere un riferimento alla libreria di estendibilità FTP per il progetto:

    • Fare clic su Progetto e quindi su Aggiungi riferimento...
    • Nella scheda .NET fare clic su Microsoft.Web.FtpServer.
    • Fare clic su OK.
  2. Aggiungere un riferimento a System.Web per il progetto:

    • Fare clic su Progetto e quindi su Aggiungi riferimento...
    • Nella scheda .NET fare clic su System.Web.
    • Fare clic su OK.
  3. Aggiungere un riferimento a System.Configuration per il progetto:

    • Fare clic su Progetto e quindi su Aggiungi riferimento...
    • Nella scheda .NET fare clic su System.Configuration.
    • Fare clic su OK.
  4. Aggiungere un riferimento a System.Data per il progetto:

    • Fare clic su Progetto e quindi su Aggiungi riferimento.
    • Nella scheda .NET fare clic su System.Data.
    • Fare clic su OK.
  5. Aggiungere il codice per la classe di autenticazione:

    • In Esplora soluzioni fare doppio clic sul file Class1.cs.

    • Rimuovere il codice esistente.

    • Incollare il codice seguente nell'editor:

      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. Salvare e compilare il progetto.

Nota

Se non sono stati usati i passaggi facoltativi per registrare gli assembly nella GAC, sarà necessario copiare manualmente gli assembly nel computer IIS e aggiungere gli assembly alla GAC usando lo strumento Gacutil.exe. Per altre informazioni, vedere Gacutil.exe (Global Assembly Cache Tool) (Strumento Global Assembly Cache, Gacutil.exe).

Passaggio 3: Aggiungere il provider demo a FTP

In questo passaggio si aggiungerà il provider demo al servizio FTP e al sito Web predefinito.

  1. Determinare le informazioni sull'assembly per il provider di estendibilità:

    • In Esplora risorse aprire il C:\Windows\assembly percorso, dove C: è l'unità del sistema operativo.
    • Individuare l'assembly FtpAddressRestrictionAuthentication .
    • Fare clic con il pulsante destro del mouse sull'assembly e quindi scegliere Proprietà.
    • Copiare il valore Culture, ad esempio Neutral.
    • Copiare il numero di versione , ad esempio 1.0.0.0.
    • Copiare il valore del token di chiave pubblica, ad esempio 426f62526f636b73.
    • Fare clic su Annulla.
  2. Usando le informazioni dei passaggi precedenti, aggiungere il provider di estendibilità all'elenco globale dei provider FTP e configurare le opzioni per il provider:

    • Al momento non esiste un'interfaccia utente che consente di aggiungere proprietà per un modulo di autenticazione personalizzata, pertanto è necessario usare la riga di comando seguente:

      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
      

    Nota

    Il stringa di connessione specificato nell'attributo connectionString deve essere un account di accesso valido per il database.

  3. Aggiungere il provider personalizzato a un sito:

    • Al momento non esiste un'interfaccia utente che consente di aggiungere funzionalità personalizzate a un sito, quindi è necessario usare la riga di comando seguente:

      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
      

    Nota

    Questa sintassi disabilita l'autenticazione DI base FTP ed è importante disabilitare l'autenticazione di base quando si usa questo provider di autenticazione. In caso contrario, quando l'indirizzo IP di un utente malintenzionato è stato bloccato da questo provider di autenticazione, un utente malintenzionato potrebbe comunque essere in grado di attaccare gli account che usano l'autenticazione di base.

  4. Aggiungere una regola di autorizzazione per il provider di autenticazione:

    • Fare doppio clic su Regole di autorizzazione FTP nella finestra principale.

    • Fare clic su Aggiungi regola consenti... nel riquadro Azioni .

    • Selezionare Utenti specificati per l'opzione di accesso.

    • Immettere un nome utente.

      Nota

      Il nome utente deve essere immesso nel database all'esterno di questo elenco di passaggi.

    • Selezionare Lettura e/o Scrittura per l'opzione Autorizzazioni .

    • Fare clic su OK.

Passaggio 4: Uso del provider con FTP 7.5

Quando i client FTP si connettono al sito FTP, il servizio FTP tenterà di autenticare gli utenti con il provider di autenticazione personalizzato usando account archiviati nel database. Se un client FTP non riesce a eseguire l'autenticazione, il provider tiene traccia dell'indirizzo IP e della data/ora dell'errore nel database. Quando un client FTP non riesce ad accedere da un indirizzo IP specifico per il numero di errori specificati nell'impostazione logonAttempts e entro l'intervallo di tempo specificato nell'impostazione floodSeconds , il provider bloccherà l'accesso all'indirizzo IP al servizio FTP.

Nota

Questo provider di esempio implementa la logica di autenticazione per il servizio FTP, ma non fornisce un modulo di amministrazione per gestire i dati nel database. Ad esempio, non è possibile gestire l'elenco di account utente FTP, indirizzi IP vietati o errori di autenticazione usando questo provider. Per gestire i dati tramite Gestione IIS, è possibile utilizzare Gestione database IIS. Per altre informazioni, vedere l'argomento seguente:

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

Informazioni aggiuntive

Per creare il database e le tabelle necessarie, è possibile usare lo script SQL seguente per Microsoft SQL Server. Per usare questo script, è necessario aggiornare il nome del database e il percorso dei file di database. In SQL Server si eseguirà lo script in una nuova finestra di query e quindi si creerà un account di accesso al database che verrà usato con il stringa di connessione.

Nota

È possibile modificare lo script SQL per archiviare il database in un percorso diverso da 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

Riepilogo

In questa procedura dettagliata si è appreso come:

  • Creare un progetto in Visual Studio 2008 per un provider FTP personalizzato.
  • Implementare le interfacce di estendibilità per un provider FTP personalizzato.
  • Aggiungere un provider personalizzato FTP al servizio FTP.