通过


使用 .NET 开发模块

作者 :Mike Volodarsky

介绍

IIS 7.0 及更高版本允许通过两种方式开发的模块扩展服务器:

  • 使用托管代码和 ASP.NET 服务器扩展性 API
  • 使用本机代码和 IIS 本机服务器扩展性 API

过去,ASP.NET 模块在功能上受到限制,因为 ASP.NET 请求处理管道独立于主服务器请求管道。

在 IIS 中,托管模块几乎与具有集成管道体系结构的本机模块一样强大。 最重要的是,托管模块提供的服务现在可以应用于服务器的所有请求,而不仅仅是对 aspX 页面等 ASP.NET 内容的请求。 托管模块的配置和管理方式与本机模块一致,并且可以在与本机模块相同的处理阶段和排序中执行。 最后,托管模块可以执行更广泛的操作集,以通过添加的多个增强 ASP.NET API 来处理请求处理。

本文说明使用托管模块扩展服务器,以便添加针对任意凭据存储执行基本身份验证的功能,例如 ASP.NET 2.0 成员身份系统中基于提供程序的凭据基础结构。

这允许将 IIS 中与 Windows 凭据存储绑定的内置基本身份验证支持替换为支持任意凭据存储的身份验证支持,或 ASP.NET 2.0 附带的现有成员身份提供程序,例如 SQL Server、SQL Express 或 Active Directory。

本文介绍以下任务:

  • 使用 ASP.NET API 开发托管模块
  • 在服务器上部署托管模块

若要了解有关开发 IIS 模块和处理程序的基础知识的详细信息,请参阅 使用 .NET Framework 开发 IIS7 模块和处理程序

还可以在博客上找到大量有关编写 IIS 模块的资源和提示, http://www.mvolo.com/以及为应用程序下载现有的 IIS 模块。 有关一些示例,请参阅 使用 HttpRedirection 模块将请求重定向到应用程序使用 DirectoryListingModule 查找 IIS 网站的目录列表,以及 使用 IconHandler 在 ASP.NET 应用程序中显示漂亮的文件图标

注释

本文中提供的代码以 C# 编写。

先决条件

若要执行本文档中的步骤,必须安装以下 IIS 功能:

ASP.NET

通过 Windows Vista 控制面板安装 ASP.NET。 选择“程序”- “打开或关闭 Windows 功能”。 然后打开“Internet Information Services”-“万维网服务”-“应用程序开发功能”,并检查“ASP.NET”。

如果你有 Windows Server® 2008 内部版本,请打开“服务器管理器”-“角色”,然后选择“Web 服务器(IIS)”。 单击“添加角色服务”。 在“应用程序开发”下,选中“ASP.NET”。

基本身份验证的背景信息

基本身份验证是在 HTTP.1 协议(RFC 2617)中定义的身份验证方案。 它使用基于挑战的标准机制,该机制在高级别工作如下:

  • 浏览器向没有凭据的 URL 发出请求
  • 如果服务器需要该 URL 的身份验证,它将使用 401 拒绝访问的消息进行响应,并包含一个标头,指示支持基本身份验证方案
  • 浏览器收到响应后,如果其设置为这样做,将会提示用户输入用户名/密码,这些信息将在下一个 URL 请求的请求标头中以纯文本形式包含其中。
  • 服务器在标头内接收用户名/密码,并将它们用于身份验证

注释

尽管本文未详细讨论此身份验证协议,但值得一提的是,基本身份验证方案要求 SSL 安全,因为它以纯文本形式发送用户名/密码。

IIS 支持对存储在本地帐户存储中的 Windows 帐户或存储在 Active Directory 中的域帐户进行基本身份验证。 我们希望让用户能够使用基本身份验证进行身份验证,但要改用 ASP.NET 2.0 成员身份 服务验证凭据。 这样就可以自由地将用户信息存储在各种现有成员身份提供程序(例如 SQL Server)中,而无需绑定到 Windows 帐户。

任务 1:使用 .NET 开发模块

在此任务中,我们将检查支持 HTTP.1 基本身份验证方案的身份验证模块的开发。 此模块是使用自 ASP.NET v1.0 以来可用的标准 ASP.NET 模块模式开发的。 此相同的模式用于生成扩展 IIS 服务器的 ASP.NET 模块。 事实上,为早期版本的 IIS 编写的现有 ASP.NET 模块可以在 IIS 上使用,并利用更好的 ASP.NET 集成为使用这些模块的 Web 应用程序提供更多功能。

注释

模块的完整代码在附录 A 中提供。

托管模块是实现 System.Web.IHttpModule 接口的 .NET 类。 此类的主要功能是注册 IIS 请求处理管道中发生的一个或多个事件,然后在 IIS 为这些事件调用模块的事件处理程序时执行一些有用的工作。

让我们创建一个名为“BasicAuthenticationModule.cs”的新源文件,并创建模块类(附录 A 中提供了完整的源代码):

public class BasicAuthenticationModule : System.Web.IHttpModule
{
    void Init(HttpApplication context)
    {
    }
    void Dispose()
    {
    }
}

Init 方法的主要功能是将模块的事件处理程序方法与相应的请求管道事件连接起来。 模块的类提供事件句柄方法,并实现模块提供的所需功能。 对此进行了更详细的讨论。

Dispose 方法用于在丢弃模块实例时清理任何模块状态。 它通常不会实现,除非模块使用需要释放的特定资源。

Init()

创建类后,下一步是实现 Init 方法。 唯一的要求是注册一个或多个请求管道事件的模块。 将遵循 System.EventHandler 委托签名的模块方法连接到在提供的 System.Web.HttpApplication 实例上公开的所需管道事件:

public void Init(HttpApplication context)            
{
   //          
   // Subscribe to the authenticate event to perform the 
   // authentication. 
   // 
   context.AuthenticateRequest += new        
              EventHandler(this.AuthenticateUser);

   // 
   // Subscribe to the EndRequest event to issue the 
   // challenge if necessary. 
   // 
   context.EndRequest += new 
              EventHandler(this.IssueAuthenticationChallenge);
}

AuthenticateRequest 事件期间,对每个请求调用 AuthenticateUser 方法。 我们利用它根据请求中存在的凭据信息对用户进行身份验证。

EndRequest 事件期间,对每个请求调用 IssueAuthenticationChallenge 方法。 它负责在授权模块拒绝请求并且需要身份验证时向客户端发出基本身份验证质询。

AuthenticateUser()

实现 AuthenticateUser 方法。 此方法执行以下操作:

  • 从传入请求标头中提取基本凭据(如果存在)。 若要查看此步骤的实现,请参阅 ExtractBasicAuthenticationCredentials 实用工具方法。
  • 尝试通过成员身份验证提供的凭据(使用配置的默认成员身份提供程序)。 若要查看此步骤的实现,请参阅 ValidateCredentials 实用工具方法。
  • 创建一个用户主体,标识用户(如果身份验证成功)并将其与请求相关联。

在此处理结束时,如果模块成功获取并验证用户凭据,它将生成经过身份验证的用户主体,其他模块和应用程序代码稍后在访问控制决策中使用。 例如,URL 授权模块会检查下一个管道事件中的用户,以便强制执行应用程序配置的授权规则。

IssueAuthenticationChallenge()

实现 IssueAuthenticationChallenge 方法。 此方法执行以下操作:

  • 检查响应状态代码以确定是否拒绝了此请求。
  • 如果是这样,请向响应发出基本身份验证质询标头,以触发客户端进行身份验证。

实用方法

实现模块使用的实用工具方法,包括:

  • ExtractBasicAuthenticationCredentials。 此方法从“Authorize”请求头中提取基本身份验证凭据,这符合基本身份验证方案的规定。
  • ValidateCredentials。 此方法尝试通过成员管理来验证用户凭据。 成员身份 API 抽象化基础凭据存储,并允许通过配置添加/删除成员资格提供程序来配置凭据存储实现。

注释

在此示例中,将注释掉成员资格验证,而模块只是检查用户名和密码是否都等于字符串“test”。 这样做是为了清楚起见,不适用于生产部署。 要启用基于会员资格的凭据验证,只需取消对 ValidateCredentials 中会员资格代码的注释,并为您的应用程序配置会员资格提供程序。 有关详细信息,请参阅附录 C。

任务 2:将模块部署到应用程序

在第一个任务中创建模块后,接下来将其添加到应用程序。

部署到应用程序

首先,将模块部署到应用程序。 此处有几种选项:

  • 将包含模块的源文件复制到应用程序的 /App_Code 目录中。 这不需要编译模块 - ASP.NET 在应用程序启动时自动编译并加载模块类型。 只需将此源代码保存为应用程序 /App_Code 目录中的BasicAuthenticationModule.cs。 如果你对其他步骤感到不舒服,请执行此操作。

  • 将模块编译为程序集,并将此程序集拖放到应用程序的 /BIN 目录中。 如果只希望此模块可用于此应用程序,并且不希望将模块的源寄送到应用程序,则这是最典型的选项。 通过从命令行提示符运行以下命令来编译模块源文件:

    <PATH_TO_FX_SDK>csc.exe /out:BasicAuthenticationModule.dll /target:library BasicAuthenticationModule.cs

    其中 <PATH_TO_FX_SDK> 包含CSC.EXE编译器的 .NET Framework SDK 的路径。

  • 将模块编译为强名称程序集,并在 GAC 中注册此程序集。 如果希望计算机上的多个应用程序使用此模块,则这是一个不错的选择。 若要了解有关生成强名称程序集的详细信息,请参阅 “创建和使用强名称程序集”。

在应用程序的 web.config 文件中进行配置更改之前,我们必须解锁默认情况下锁定在服务器级别的部分。 从提升的命令提示符运行以下命令( > 右键单击 Cmd.exe 并选择“以管理员身份运行”):

%windir%\system32\inetsrv\APPCMD.EXE unlock config /section:windowsAuthentication
%windir%\system32\inetsrv\APPCMD.EXE unlock config /section:anonymousAuthentication

运行这些命令后,你将能够在应用程序的 web.config 文件中定义这些配置节。

将模块配置为在应用程序中运行。 首先创建新的 web.config 文件,其中包含启用和使用新模块所需的配置。 首先添加以下文本并将其保存到应用程序的根目录(%systemdrive%\inetpub\wwwroot\web.config 如果在默认网站中使用根应用程序)。

<configuration> 
    <system.webServer> 
        <modules> 
        </modules> 
        <security> 
            <authentication> 
                <windowsAuthentication enabled="false"/> 
                <anonymousAuthentication enabled="false"/> 
            </authentication> 
        </security> 
    </system.webServer> 
</configuration>

启用新的基本身份验证模块之前,请禁用所有其他 IIS 身份验证模块。 默认情况下,仅启用 Windows 身份验证和匿名身份验证。 由于我们不希望浏览器尝试使用 Windows 凭据进行身份验证或允许匿名用户,因此我们禁用 Windows 身份验证模块和匿名身份验证模块。

现在,通过将模块添加到应用程序加载的模块列表中来启用该模块。 再次打开 web.config,并将条目添加到 <modules> 标记中

<add name="MyBasicAuthenticationModule" type="IIS7Demos.BasicAuthenticationModule" />

还可以使用 IIS 管理工具或APPCMD.EXE命令行工具部署模块。

附录 B 中提供了这些更改后应用程序 web.config 文件的最终内容。

恭喜,你已完成自定义基本身份验证模块的配置。

我们来试一试! 打开 Internet Explorer,并在以下 URL 向应用程序发出请求:

http://localhost/

应会看到基本身份验证登录对话框。 在“用户名:”字段中输入“test”,并在“密码:”字段中输入“test”以获取访问权限。 请注意,如果将 HTML、JPG 或任何其他内容复制到应用程序,它们也将受到新的 BasicAuthenticationModule 的保护。

总结

本文介绍了如何为应用程序开发和部署自定义托管模块,并使该模块可为应用程序的所有请求提供服务。

你还见证了在托管代码中开发服务器组件的强大功能。 这允许开发与 Windows 凭据存储分离的基本身份验证服务。

如果你是冒险的,请将此模块配置为利用 ASP.NET 2.0 成员身份应用程序服务的强大功能来支持可插入凭据存储。 有关详细信息,请参阅附录 C。

在博客中查找有关编写 IIS 模块的许多资源和提示, http://www.mvolo.com/以及为应用程序下载现有的 IIS 模块。 有关一些示例,请参阅 使用 HttpRedirection 模块将请求重定向到应用程序使用 DirectoryListingModule 查找 IIS 网站的目录列表,以及 使用 IconHandler 在 ASP.NET 应用程序中显示漂亮的文件图标

附录 A:基本身份验证模块源代码

将此源代码保存为 /App_Code 目录中的BasicAuthenticationModule.cs,以便将其快速部署到应用程序。

注释

如果使用记事本,请确保设置“另存为:所有文件”以避免将文件另存为 BasicAuthenticationModule.cs.txt。

#region Using directives
using System;
using System.Collections;
using System.Text;
using System.Web;
using System.Web.Security;
using System.Security.Principal;
using System.IO;
#endregion
 
namespace IIS7Demos
{
    /// 
    /// This module performs basic authentication. 
    /// For details on basic authentication see RFC 2617. 
    /// 
    /// The basic operational flow is: 
    /// 
    ///     On AuthenticateRequest: 
    ///         extract the basic authentication credentials 
    ///         verify the credentials 
    ///         if succesfull, create the user principal with these credentials 
    /// 
    ///     On SendResponseHeaders: 
    ///         if the request is being rejected with an unauthorized status code (401), 
    ///         add the basic authentication challenge to trigger basic authentication. 
    ///       
    /// 

    public class BasicAuthenticationModule : IHttpModule
    {
        #region member declarations
        public const String     HttpAuthorizationHeader = "Authorization";  // HTTP1.1 Authorization header 
        public const String     HttpBasicSchemeName = "Basic"; // HTTP1.1 Basic Challenge Scheme Name 
        public const Char       HttpCredentialSeparator = ':'; // HTTP1.1 Credential username and password separator 
        public const int        HttpNotAuthorizedStatusCode = 401; // HTTP1.1 Not authorized response status code 
        public const String     HttpWWWAuthenticateHeader = "WWW-Authenticate"; // HTTP1.1 Basic Challenge Scheme Name 
        public const String     Realm = "demo"; // HTTP.1.1 Basic Challenge Realm 
        #endregion

        #region Main Event Processing Callbacks
        public void AuthenticateUser(Object source, EventArgs e)
        {
            HttpApplication application = (HttpApplication)source;
            HttpContext context = application.Context;
            String userName = null;
            String password = null;
            String realm = null;
            String authorizationHeader = context.Request.Headers[HttpAuthorizationHeader];

            // 
            //  Extract the basic authentication credentials from the request 
            // 
            if (!ExtractBasicCredentials(authorizationHeader, ref userName, ref password))
                return;
            // 
            // Validate the user credentials 
            // 
            if (!ValidateCredentials(userName, password, realm))
               return;

            // 
            // Create the user principal and associate it with the request 
            // 
            context.User = new GenericPrincipal(new GenericIdentity(userName), null);
        }

        public void IssueAuthenticationChallenge(Object source, EventArgs e)
        {
            HttpApplication application = (HttpApplication)source;
            HttpContext context = application.Context;

            // 
            // Issue a basic challenge if necessary 
            // 

            if (context.Response.StatusCode == HttpNotAuthorizedStatusCode)
            {
                context.Response.AddHeader(HttpWWWAuthenticateHeader, "Basic realm =\"" + Realm + "\"");
            }
        }
        #endregion

        #region Utility Methods
        protected virtual bool ValidateCredentials(String userName, String password, String realm)
        {
            // 
            //  Validate the credentials using Membership (refault provider) 
            // 
            // NOTE: Membership is commented out for clarity reasons.   
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
            // WARNING: DO NOT USE THE CODE BELOW IN PRODUCTION 
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
            // return Membership.ValidateUser(userName, password); 
            if (userName.Equals("test") && password.Equals("test"))
            {
                return true;
            }
            else 
            {
                return false;
            }    
        }
      
        protected virtual bool ExtractBasicCredentials(String authorizationHeader, ref String username, ref String password)
        {
            if ((authorizationHeader == null) || (authorizationHeader.Equals(String.Empty)))
               return false;
            String verifiedAuthorizationHeader = authorizationHeader.Trim();
            if (verifiedAuthorizationHeader.IndexOf(HttpBasicSchemeName) != 0)     
                return false;

            // get the credential payload 
            verifiedAuthorizationHeader = verifiedAuthorizationHeader.Substring(HttpBasicSchemeName.Length, verifiedAuthorizationHeader.Length - HttpBasicSchemeName.Length).Trim();
           // decode the base 64 encoded credential payload 
            byte[] credentialBase64DecodedArray = Convert.FromBase64String(verifiedAuthorizationHeader);
            UTF8Encoding encoding = new UTF8Encoding();
            String decodedAuthorizationHeader = encoding.GetString(credentialBase64DecodedArray, 0, credentialBase64DecodedArray.Length);

            // get the username, password, and realm 
            int separatorPosition = decodedAuthorizationHeader.IndexOf(HttpCredentialSeparator);

           if (separatorPosition <= 0)
              return false;
            username = decodedAuthorizationHeader.Substring(0, separatorPosition).Trim();
           password = decodedAuthorizationHeader.Substring(separatorPosition + 1, (decodedAuthorizationHeader.Length - separatorPosition - 1)).Trim();

            if (username.Equals(String.Empty) || password.Equals(String.Empty))
               return false;

           return true;
        }
        #endregion

        #region IHttpModule Members
        public void Init(HttpApplication context)
        {
            // 
            // Subscribe to the authenticate event to perform the 
            // authentication. 
            // 
            context.AuthenticateRequest += new 
                               EventHandler(this.AuthenticateUser);
            // 
            // Subscribe to the EndRequest event to issue the 
            // challenge if necessary. 
            // 
            context.EndRequest += new 
                               EventHandler(this.IssueAuthenticationChallenge);
        }
        public void Dispose()
        {
            // 
            // Do nothing here 
            // 
        }
        #endregion

    }
}

附录 B:基本身份验证模块的 Web.config

将此配置另存为应用程序根目录中 web.config 文件:

<configuration> 
    <system.webServer> 
      <modules> 
           <add name="MyBasicAuthenticationModule" type="IIS7Demos.BasicAuthenticationModule" /> 
      </modules> 
      <security> 
         <authentication> 
          <windowsAuthentication enabled="false"/> 
             <anonymousAuthentication enabled="false"/> 
         </authentication> 
      </security> 
    </system.webServer> 
</configuration>

附录 C:配置成员身份

ASP.NET 2.0 成员身份服务使应用程序能够快速实现大多数身份验证和访问控制方案所需的凭据验证和用户管理。 成员身份将应用程序代码与实际的凭据存储实现隔离开来,并提供许多用于与现有凭据存储集成的选项。

要充分利用此模块示例的成员资格,请在 ValidateCredentials 方法中将对 Membership.ValidateUser 的调用取消注释处理,并为您的应用程序配置一个成员资格提供程序。 有关配置成员身份的详细信息,请参阅 配置 ASP.NET 应用程序以使用成员身份