Microsoft 创建了一个新的 FTP 服务,该服务针对 Windows Server® 2008 完全重新编写。 这种新的 FTP 服务包含许多新功能,使 Web 作者能够比以前更轻松地发布内容,并为 Web 管理员提供了更多的安全性和部署选项。
新的 FTP 7.5 服务支持扩展性,让你扩展 FTP 服务随附的内置功能。 更具体地说,FTP 7.5 支持创建你自己的身份验证提供程序。 你还可以创建用于自定义 FTP 日志记录和确定 FTP 用户的主目录信息的提供程序。
本演练将引导你使用托管代码创建 FTP 身份验证提供程序,该提供程序对使用 SQL Server 数据库存储帐户信息的动态 IP 限制提供支持。 该提供程序实现此逻辑的方式是记录远程 IP 地址的失败次数,然后使用此信息阻止在给定时间范围内无法登录服务器的 IP 地址。
重要
要在本演练中使用提供程序,必须安装最新版本的 FTP 7.5 服务。 2009 年 8 月 3 日发布的 FTP 7.5 版本解决了 IFtpLogProvider.Log() 方法中本地和远程 IP 地址不正确的问题。 因此,使用更早版本的 FTP 服务将导致该提供程序无法运行。
先决条件
必须准备好以下各项才能完成本文中的过程:
IIS 7.0 或更高版本必须安装在 Windows Server 2008 服务器上,并且必须安装 Internet Information Services (IIS) 管理器。
必须安装新的 FTP 7.5 服务。
重要
如本演练前面所述,要在本演练中使用提供程序,必须安装最新版本的 FTP 7.5 服务。 2009 年 8 月 3 日发布的 FTP 7.5 版本解决了 IFtpLogProvider.Log() 方法中本地和远程 IP 地址不正确的问题。 因此,使用更早版本的 FTP 服务将导致该提供程序无法运行。
必须为站点启用 FTP 发布。
必须使用 Visual Studio 2008。
注意
如果你使用早期版本的 Visual Studio,则本演练中的某些步骤可能不正确。
必须为用户帐户列表和相关限制列表使用 SQL Server 数据库;此示例不能与 FTP 基本身份验证一起使用。 本演练的“更多信息”部分包含用于创建此示例所需表的 SQL Server 脚本。
IIS 计算机上需要 Gacutil.exe;这是将程序集添加到全局程序集缓存 (GAC) 所必需的。
重要
为帮助提高身份验证请求的性能,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
关闭命令提示符。
完成这些更改后,此示例中的身份验证提供程序将能够立即拒绝潜在攻击者的所有请求。
提供商说明
本演练包含几个需要讨论的要点。 基于 Internet 的攻击通常会利用 FTP 服务器,试图获取系统上帐户的用户名和密码。 检测这种行为的可行方法是分析 FTP 活动、检查用于攻击系统的 IP 地址并阻止这些地址今后的访问。 遗憾的是,这是一个手动过程,即使该过程是自动化的,也不会是实时的。
FTP 服务提供基于 IP 地址限制连接的功能,但 IP 地址列表存储在 IIS 配置文件中,需要管理访问权限才能更新。 FTP 服务的扩展性过程以低特权帐户运行,该帐户无权更新 IIS 配置文件的必需设置。 可编写 FTP 日志记录提供程序来检测用户名淹没攻击并将该信息写入数据存储或以较高特权帐户(可更新 IIS 配置文件)运行的单独服务,但这需要更了解系统体系结构,并需要大量困难的实现详细信息。 因此,有必要使用备用数据存储。
数据库是一个理想选择,因为便于访问数据,而且数据库中的数据操作工具也很普遍。 下一个挑战是使用现有的 FTP 扩展性接口来实现必要逻辑,以检测攻击者可能使用的登录淹没攻击。 回顾一下,可用的扩展性接口包括:
可以轻松编写一个利用所有这些接口的提供程序,以便更大程度地提高安全性,但本演练中的提供程序只使用以下接口:
- IFtpAuthenticationProvider - 提供程序将使用此接口允许或拒绝对 FTP 服务器的访问。
- IFtpLogProvider - 提供程序将使用此接口作为通用事件侦听器。
FTP 服务没有提供程序可注册的实际事件通知,但你可以编写使用 IFtpLogProvider.Log() 方法来提供事件后处理的提供程序。 例如,任何失败的登录尝试都将记录“PASS”命令并提供“230”(这是成功 FTP 登录的状态代码)以外的状态代码。 通过捕获有关失败登录尝试的更多信息(如登录失败的客户端的 IP 地址),可以利用这些信息提供更多功能,例如阻止 IP 地址将来访问 FTP 服务器。
提供程序体系结构和逻辑
以下说明总结了此身份验证提供程序的行为:
在系统上注册提供程序时,在 IIS 配置文件中指定要使用的数据库连接,以及失败登录尝试次数和淹没攻击超时的值。
当 FTP 服务加载提供程序时,它会将 IIS 配置文件中的值提供给提供程序的 Initialize() 方法。 这些值存储在全局设置中后,Initialize() 方法会执行一些初始垃圾回收,以清理数据库中可能存在的来自以前 FTP 会话的所有信息。
当 FTP 客户端连接到 FTP 服务器时,FTP 服务会向提供程序的 Log() 方法发送“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 扩展性库的引用路径:
单击“项目”,然后单击“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
单击“添加文件夹”。
为项目添加一个强名称密钥:
- 单击“项目”,然后单击“FtpAddressRestrictionAuthentication 属性”。
- 单击“签名”选项卡。
- 勾选“为程序集签名”复选框。
- 从强密钥名称下拉框中选择“<新建...>”。
- 输入 FtpAddressRestrictionAuthenticationKey 作为密钥文件名。
- 如果需要,为密钥文件输入一个密码;否则,请清除“使用密码保护我的密钥文件”复选框。
- 单击“确定”。
可选:可以添加自定义生成事件,以将 DLL 自动添加到开发计算机上的全局程序集缓存 (GAC):
单击“项目”,然后单击“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:\Windows\assembly
路径,其中 C: 是操作系统驱动器。 - 找到 FtpAddressRestrictionAuthentication 程序集。
- 右键单击该程序集,然后单击“属性”。
- 复制“区域性”值,例如“Neutral”。
- 复制“版本”号,例如“1.0.0.0”。
- 复制“公钥令牌”值,例如“426f62526f636b73”。
- 单击“取消” 。
- 在 Windows 资源管理器中打开
使用前面步骤中的信息,将扩展性提供程序添加到 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 客户端无法从特定 IP 地址登录,原因是达到 logonAttempts 设置中指定的失败次数且在 floodSeconds 设置中指定的时间范围内,提供程序将阻止该 IP 地址登录 FTP 服务。
注意
此示例提供程序实现了 FTP 服务的身份验证逻辑,但不提供管理模块来管理数据库中的数据。 例如,不能使用此提供程序管理 FTP 用户帐户、已禁止 IP 地址或身份验证失败的列表。 要使用 IIS 管理器管理这些数据,可使用 IIS 数据库管理器。 有关详细信息,请参阅以下主题:
其他信息
可使用以下 SQL 脚本在 Microsoft SQL Server 中创建所需的数据库和表。 要使用此脚本,需要更新数据库名称以及数据库文件的位置。 在 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 服务。