使用 .NET 开发模块

作者:Mike Volodarsky

介绍

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

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

过去,ASP.NET 模块的功能受到限制,因为 ASP.NET 请求处理管道与主服务器请求管道是分开的。

在 IIS 中,托管模块实际上变得与具有集成管道体系结构的本机模块一样强大。 最重要的是,托管模块提供的服务现在可以应用于对服务器发出的所有请求,而不仅仅是对 ASP.NET 内容(例如 ASPX 页)的请求。 托管模块以与本机模块一致的方式进行配置和管理,并且可以在与本机模块相同的处理阶段和顺序规则中执行。 最后,托管模块可以执行一组更广泛的操作,通过几个新增和增强的 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 模块和处理程序

还可以在博客 http://www.mvolo.com/ 中找到有关编写 IIS 模块的大量资源和提示,以及下载适用于你的应用程序的现有 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。 此方法从授权请求头中提取基本身份验证凭据,如基本身份验证方案中所指定。
  • 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。

可以在博客 http://www.mvolo.com/ 中找到有关编写 IIS 模块的许多资源和提示,以及下载适用于你的应用程序的现有 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 应用程序以使用成员资格