동적 IP 제한을 사용하여 FTP 인증 공급자 만들기
작성자: 로버트 맥머레이
Microsoft는 Windows Server® 2008용으로 완전히 다시 작성된 새 FTP 서비스를 만들었습니다. 이 새로운 FTP 서비스는 웹 작성자가 이전보다 더 쉽게 콘텐츠를 게시할 수 있는 많은 새로운 기능을 통합하고 웹 관리자에게 더 많은 보안 및 배포 옵션을 제공합니다.
새 FTP 7.5 서비스는 FTP 서비스에 포함된 기본 제공 기능을 확장할 수 있는 확장성을 지원합니다. 보다 구체적으로 FTP 7.5는 사용자 고유의 인증 공급자 만들기를 지원합니다. 사용자 지정 FTP 로깅 및 FTP 사용자의 홈 디렉터리 정보를 결정하는 공급자를 만들 수도 있습니다.
이 연습에서는 SQL Server 데이터베이스를 사용하여 계정 정보를 저장하는 동적 IP 제한에 대한 지원을 제공하는 FTP 인증 공급자에 관리 코드를 사용하는 단계를 안내합니다. 이 공급자는 원격 IP 주소에서 오류 수를 로깅한 다음 이 정보를 사용하여 지정된 시간 프레임 내에 서버에 로그인하지 못하는 IP 주소를 차단하여 이 논리를 구현합니다.
Important
이 연습에서 공급자를 사용하려면 최신 버전의 FTP 7.5 서비스를 설치해야 합니다. 2009년 8월 3일에 IFtpLogProvider.Log() 메서드의 로컬 및 원격 IP 주소가 잘못된 문제를 해결한 버전 FTP 7.5가 릴리스되었습니다. 이로 인해 이전 버전의 FTP 서비스를 사용하면 이 공급자가 작동하지 않습니다.
필수 조건
이 문서의 절차를 완료하려면 다음 항목이 필요합니다.
IIS 7.0 이상은 Windows Server 2008 서버에 설치해야 하며 IIS(인터넷 정보 서비스) 관리자도 설치해야 합니다.
새 FTP 7.5 서비스를 설치해야 합니다.
Important
이 연습의 앞부분에서 멘션 이 연습에서 공급자를 사용하려면 최신 버전의 FTP 7.5 서비스를 설치해야 합니다. 2009년 8월 3일에 IFtpLogProvider.Log() 메서드의 로컬 및 원격 IP 주소가 잘못된 문제를 해결한 버전 FTP 7.5가 릴리스되었습니다. 이로 인해 이전 버전의 FTP 서비스를 사용하면 이 공급자가 작동하지 않습니다.
사이트에 대해 FTP 게시를 사용하도록 설정해야 합니다.
Visual Studio 2008을 사용해야 합니다.
참고 항목
이전 버전의 Visual Studio를 사용하는 경우 이 연습의 일부 단계가 올바르지 않을 수 있습니다.
사용자 계정 목록 및 관련 제한 목록에 SQL Server 데이터베이스를 사용해야 합니다. 이 예제는 FTP 기본 인증에 사용할 수 없습니다. 이 연습의 "추가 정보" 섹션에는 이 샘플에 필요한 테이블을 만드는 SQL Server용 스크립트가 포함되어 있습니다.
IIS 컴퓨터에서 Gacutil.exe 필요합니다. GAC(전역 어셈블리 캐시)에 어셈블리를 추가하는 데 필요합니다.
Important
인증 요청의 성능을 향상시키기 위해 FTP 서비스는 기본적으로 15분 동안 성공적인 로그인을 위해 자격 증명을 캐시합니다. 이 인증 공급자는 공격자의 요청을 즉시 거부하지만 공격자가 최근에 로그인한 사용자의 암호를 성공적으로 추측할 수 있는 경우 캐시된 자격 증명을 통해 액세스 권한을 얻을 수 있습니다. 이 공급자가 IP 주소를 차단한 후 악의적인 사용자가 서버를 공격할 수 있도록 허용하면 의도하지 않은 결과가 발생할 수 있습니다. 이러한 잠재적 공격 방법을 완화하려면 FTP 서비스에 대한 자격 증명 캐싱을 사용하지 않도록 설정해야 합니다. 이렇게 하려면 다음 단계를 수행합니다.
명령 프롬프트가 엽니다.
다음 명령을 입력합니다.
cd /d "%SystemRoot%\System32\Inetsrv" Appcmd.exe set config -section:system.ftpServer/caching /credentialsCache.enabled:"False" /commit:apphost Net stop FTPSVC Net start FTPSVC
명령 프롬프트를 닫습니다.
이러한 변경을 수행한 후 이 예제의 인증 공급자는 잠재적인 공격자의 모든 요청을 즉시 거부할 수 있습니다.
공급자 설명
이 연습에는 몇 가지 논의가 필요한 몇 가지 사항이 포함되어 있습니다. 인터넷 기반 공격은 시스템의 계정에 대한 사용자 이름과 암호를 얻기 위해 FTP 서버를 악용하는 경우가 많습니다. FTP 활동 로그를 분석하고 시스템을 공격하는 데 사용되는 IP 주소를 검사하고 향후 액세스에서 해당 주소를 차단하여 이 동작을 검색할 수 있습니다. 아쉽게도 이는 수동 프로세스이며, 해당 프로세스가 자동화된 경우에도 실시간이 아닙니다.
FTP 서비스에는 IP 주소에 따라 연결을 제한하는 기능이 포함되어 있지만 IP 주소 목록은 IIS 구성 파일에 저장되며 업데이트하려면 관리 액세스 권한이 필요합니다. FTP 서비스에 대한 확장성 프로세스는 IIS 구성 파일의 필수 설정을 업데이트할 수 있는 권한이 없는 낮은 권한 계정으로 실행됩니다. 사용자 이름 홍수를 감지하고 해당 정보를 데이터 저장소 및 IIS 구성 파일을 업데이트할 수 있는 더 높은 권한의 계정으로 실행되는 별도의 서비스에 쓰는 FTP 로깅 공급자를 작성할 수 있지만 시스템 아키텍처에 대한 자세한 지식이 필요하고 여러 가지 어려운 구현 세부 정보가 필요합니다. 이 때문에 대체 데이터 저장소가 필요합니다.
데이터베이스는 데이터 액세스의 용이성과 데이터베이스의 데이터를 조작하는 데 사용할 수 있는 도구의 일반 공급으로 인해 이상적인 선택입니다. 다음 과제는 기존 FTP 확장성 인터페이스를 사용하여 공격자가 사용할 로그인 홍수를 감지하는 데 필요한 논리를 구현하는 것입니다. 검토를 통해 사용 가능한 확장성 인터페이스는 다음과 같습니다.
- IFtpAuthenticationProvider 인터페이스
- IFtpHomeDirectoryProvider 인터페이스
- IFtpLogProvider 인터페이스
- IFtpRoleProvider 인터페이스
보안을 강화하기 위해 이러한 모든 인터페이스를 활용하는 공급자를 쉽게 작성할 수 있지만 이 연습의 공급자는 다음 인터페이스만 사용합니다.
- IFtpAuthenticationProvider - 공급자는 이 인터페이스를 사용하여 FTP 서버에 대한 액세스를 허용하거나 거부합니다.
- IFtpLogProvider - 공급자는 이 인터페이스를 제네릭 이벤트 수신기로 사용합니다.
FTP 서비스에는 공급자가 등록할 수 있는 실제 이벤트 알림이 없지만 IFtpLogProvider.Log() 메서드를 사용하여 이벤트 후 처리를 제공하는 공급자를 작성할 수 있습니다. 예를 들어 실패한 로그인 시도는 성공적인 FTP 로그인에 대한 상태 코드인 "230"이 아닌 상태 코드로 "PASS" 명령을 기록합니다. 로그인에 실패한 클라이언트의 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에서 프로젝트를 만듭니다.
Microsoft Visual Studio 2008을 엽니다.
[파일] 메뉴를 클릭한 다음 [새로 만들기], [프로젝트]를 차례로 클릭합니다.
새 프로젝트 대화 상자에서 다음을 수행합니다.
- 프로젝트 형식으로 Visual C#을 선택합니다.
- 템플릿으로 클래스 라이브러리를 선택합니다.
- FtpAddressRestrictionAuthentication을 프로젝트의 이름으로 입력합니다.
- 확인을 클릭합니다.
프로젝트가 열리면 FTP 확장성 라이브러리에 참조 경로를 추가합니다.
Project를 클릭한 다음 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
폴더 추가를 클릭합니다.
프로젝트에 강력한 이름 키를 추가합니다.
- Project를 클릭한 다음 FtpAddressRestrictionAuthentication 속성을 클릭합니다.
- 시그니처 탭을 클릭합니다.
- 어셈블리 검사 서명 상자를 선택합니다.
- 강력한 키 이름 드롭다운 상자에서 새로> 만들기를 선택합니다<.
- 키 파일 이름에 FtpAddressRestrictionAuthenticationKey를 입력합니다.
- 원하는 경우 키 파일의 암호를 입력합니다. 그렇지 않으면 암호 검사 상자로 내 키 파일 보호의 선택 취소합니다.
- 확인을 클릭합니다.
선택 사항: 사용자 지정 빌드 이벤트를 추가하여 DLL을 개발 컴퓨터의 GAC(전역 어셈블리 캐시)에 자동으로 추가할 수 있습니다.
Project를 클릭한 다음 FtpAddressRestrictionAuthentication 속성을 클릭합니다.
빌드 이벤트 탭을 클릭합니다.
빌드 후 이벤트 명령줄 대화 상자에 다음을 입력합니다.
net stop ftpsvc call "%VS90COMNTOOLS%\vsvars32.bat">null gacutil.exe /if "$(TargetPath)" net start ftpsvc
프로젝트를 저장합니다.
2단계: 확장성 클래스 만들기
이 단계에서는 데모 공급자에 대한 로깅 확장성 인터페이스를 구현합니다.
프로젝트의 FTP 확장성 라이브러리에 대한 참조를 추가합니다.
- 프로젝트를 클릭한 다음 참조 추가...
- .NET 탭에서 Microsoft.Web.FtpServer를 클릭합니다.
- 확인을 클릭합니다.
프로젝트에 대한 System.Web에 대한 참조를 추가합니다.
- 프로젝트를 클릭한 다음 참조 추가...
- .NET 탭에서 System.Web을 클릭합니다.
- 확인을 클릭합니다.
프로젝트에 대한 System.Configuration에 대한 참조를 추가합니다.
- 프로젝트를 클릭한 다음 참조 추가...
- .NET 탭에서 System.Configuration을 클릭합니다.
- 확인을 클릭합니다.
프로젝트의 System.Data에 대한 참조를 추가합니다.
- 프로젝트를 클릭한 다음 참조 추가를 클릭합니다.
- .NET 탭에서 System.Data를 클릭합니다.
- 확인을 클릭합니다.
인증 클래스에 대한 코드를 추가합니다.
솔루션 탐색기 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); } } }
프로젝트를 저장하고 컴파일합니다.
참고 항목
선택적 단계를 사용하여 GAC에 어셈블리를 등록하지 않은 경우 수동으로 어셈블리를 IIS 컴퓨터에 복사하고 Gacutil.exe 도구를 사용하여 GAC에 어셈블리를 추가해야 합니다. 자세한 내용은 Gacutil.exe(전역 어셈블리 캐시 도구)를 참조하세요.
3단계: FTP에 데모 공급자 추가
이 단계에서는 FTP 서비스 및 기본 웹 사이트에 데모 공급자를 추가합니다.
확장성 공급자에 대한 어셈블리 정보를 확인합니다.
- Windows 탐색기에서 C: 운영 체제 드라이브인 경로를 엽니다
C:\Windows\assembly
. - FtpAddressRestrictionAuthentication 어셈블리를 찾습니다.
- 어셈블리를 마우스 오른쪽 단추로 클릭한 다음 속성을 클릭합니다.
- 문화권 값을 복사합니다(예: 중립).
- 버전 번호(예: 1.0.0.0)를 복사합니다.
- 공개 키 토큰 값을 복사합니다(예: 426f62526f636b73).
- 취소를 클릭합니다.
- Windows 탐색기에서 C: 운영 체제 드라이브인 경로를 엽니다
이전 단계의 정보를 사용하여 확장성 공급자를 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 특성에 지정하는 연결 문자열 데이터베이스에 유효한 로그인이어야 합니다.
사이트에 사용자 지정 공급자를 추가합니다.
현재 사이트에 사용자 지정 기능을 추가할 수 있는 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 주소가 차단된 경우에도 공격자는 기본 인증을 사용하는 계정을 공격할 수 있습니다.
인증 공급자에 대한 권한 부여 규칙을 추가합니다.
기본 창에서 FTP 권한 부여 규칙을 두 번 클릭합니다.
작업 창에서 허용 규칙 추가...를 클릭합니다.
액세스 옵션에 대해 지정된 사용자를 선택합니다.
사용자 이름을 입력합니다.
참고 항목
사용자 이름은 이 단계 목록 외부의 데이터베이스에 입력해야 합니다.
사용 권한 옵션에 대해 읽기 및/또는 쓰기를 선택합니다.
확인을 클릭합니다.
4단계: FTP 7.5에서 공급자 사용
FTP 클라이언트가 FTP 사이트에 연결하는 경우 FTP 서비스는 데이터베이스에 저장된 계정을 사용하여 사용자 지정 인증 공급자로 사용자를 인증하려고 시도합니다. FTP 클라이언트가 인증에 실패하면 공급자는 데이터베이스에서 오류의 IP 주소와 날짜/시간을 추적합니다. FTP 클라이언트가 logonAttempts 설정에 지정된 오류 수에 대한 특정 IP 주소에서 로그인하지 못하고 floodSeconds 설정에 지정된 시간 프레임 내에서 공급자가 IP 주소가 FTP 서비스에 로그인하지 못하도록 차단합니다.
참고 항목
이 샘플 공급자는 FTP 서비스에 대한 인증 논리를 구현하지만 데이터베이스의 데이터를 관리하는 관리 모듈을 제공하지는 않습니다. 예를 들어 이 공급자를 사용하여 FTP 사용자 계정, 금지된 IP 주소 또는 인증 실패 목록을 관리할 수 없습니다. IIS 관리자를 사용하여 데이터를 관리하려면 IIS 데이터베이스 관리자 사용할 수 있습니다. 자세한 내용은
추가 정보
Microsoft SQL Server용 다음 SQL 스크립트를 사용하여 필요한 데이터베이스 및 테이블을 만들 수 있습니다. 이 스크립트를 사용하려면 데이터베이스의 이름과 데이터베이스 파일의 위치를 업데이트해야 합니다. SQL Server에서 새 쿼리 창에서 스크립트를 실행한 다음 연결 문자열 사용할 데이터베이스 로그인을 만듭니다.
참고 항목
데이터베이스를 다른 c:\databases
위치에 저장하도록 SQL 스크립트를 변경할 수 있습니다.
/****** 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
요약
이 연습에서는 다음을 실행하는 방법을 알아보았습니다.
- 사용자 지정 FTP 공급자를 위한 프로젝트를 Visual Studio 2008에서 만듭니다.
- 사용자 지정 FTP 공급자에 대한 확장성 인터페이스를 구현합니다.
- FTP 서비스에 FTP 사용자 지정 공급자를 추가합니다.