如何利用 IIS 7.0 集成管道

作者:Mike Volodarsky

IIS 6.0 和先前版本允许通过 ASP.NET 平台开发 .NET 应用程序组件。 ASP.NET 已通过 ISAPI 扩展与 IIS 集成,并已公开自己的应用程序和请求处理模型。 这有效地公开了两个独立的服务器管道,一个用于本机 ISAPI 筛选器和扩展组件,另一个用于托管应用程序组件。 ASP.NET 组件将完全在 ASP.NET ISAPI 扩展气泡中执行,并且仅对 IIS 脚本映射配置中映射到 ASP.NET 的请求执行。

IIS 7.0 及更高版本将 ASP.NET 运行时与核心 Web 服务器集成,提供了向称为模块的本地和托管组件公开的统一请求处理管道。 集成的许多好处包括:

  • 无需考虑处理程序,允许本机和托管模块提供的服务应用于所有请求。 例如,托管窗体身份验证可用于所有内容,包括 ASP 页面、CGI 和静态文件。
  • 使 ASP.NET 组件能够提供其早先因位于服务器管道而无法使用的功能。 例如,提供请求重写功能的托管模块可以在任何服务器处理(包括身份验证)之前重写请求。
  • 可在一个位置实现、配置、监视和支持服务器功能,如单一模块和处理程序映射配置、单一自定义错误配置、单一 url 授权配置。

本文介绍 ASP.NET 应用程序如何利用 IIS 7.0 及更高版本中的集成模式,并使用以下任务举例说明:

  • 在每应用程序级别上启用/禁用模块。
  • 将托管应用程序模块添加到服务器,并启用以应用于所有请求类型。
  • 添加托管处理程序。

有关生成 IIS 7.0 及更高版本模块的详细信息,请参阅使用 .NET Framework 开发 IIS 7.0 及更高版本模块和处理程序

另请参阅博客 http://www.mvolo.com/,了解在 IIS 7.0 及更高版本中利用集成模式以及开发利用 ASP.NET 集成的 IIS 模块的更多技巧。 下载一些此类模块,包括使用 HttpRedirection 模块将请求重定向到应用程序使用 DirectoryListingModule 为 IIS 网站提供美观的目录列表使用 IconHandler 在 ASP.NET 应用程序中显示漂亮的文件图标

先决条件

要按本文档中的步骤操作,必须安装以下 IIS 7.0 及更高版本的功能。

ASP.NET

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

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

经典 ASP

我们想展示 ASP.NET 模块现在如何适用于所有内容,而不仅仅是适用于 ASP.NET 页面,因此通过 Windows Vista 控制面板安装经典 ASP。 选择“程序”-“打开或关闭 Windows 功能”。 然后打开“Internet Information Services”-“万维网服务”-“应用程序开发功能”,选中“ASP”。

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

将窗体身份验证添加到应用程序

作为此任务的一部分,我们为应用程序启用基于 ASP.NET 窗体的身份验证。 在下一个任务中,无论内容类型如何,我们都将为对应用程序的所有请求启用窗体身份验证模块。

首先,按对普通 ASP.NET 应用程序的方式配置窗体身份验证。

创建示例页面

为说明该功能,我们在 web 根目录中添加 default.aspx 页面。 打开记事本(为确保可访问以下 wwwroot 目录,必须以管理员身份运行 - 右键单击 Programs\Accessories\Notepad 图标,然后单击“以管理员身份运行”),并创建以下文件:%systemdrive%\inetpub\wwwroot\default.aspx。 将以下行粘贴到该文件中:

<%=Datetime.Now%> 
<BR> 
Login Name: <asp:LoginName runat="server"/>

default.aspx 只显示当前时间和登录用户的姓名。 稍后,我们将使用此页来展示窗体身份验证。

配置窗体身份验证和访问控制规则

现在,使用窗体身份验证来保护 default.aspx。 在 %systemdrive%\inetpub\wwwroot 目录中创建 web.config 文件,并添加如下配置:

<configuration> 
  <system.web> 
    <!--membership provider entry goes here--> 
    <authorization> 
      <deny users="?"/> 
      <allow users="*"/> 
    </authorization> 
    <authentication mode="Forms"/> 
  </system.web> 
</configuration>

此配置将 ASP.NET 身份验证模式设置为使用基于窗体的身份验证,并添加授权设置来控制对应用程序的访问。 这些设置拒绝匿名用户 (?) 访问,仅允许经身份验证的用户 (*) 访问。

创建成员资格提供程序

步骤 1:我们必须提供验证用户凭据的身份验证存储。 为说明 ASP.NET 与 IIS 7.0 及更高版本之间的深度集成,我们使用我们自己的基于 XML 的成员资格提供程序(如果已安装 SQL Server,你也可以使用默认的 SQL Server 成员资格提供程序)。

在 web.config 文件的初始 <configuration>/<system.web> 配置元素后添加以下条目:

<membership defaultProvider="AspNetReadOnlyXmlMembershipProvider"> 
  <providers> 
    <add name="AspNetReadOnlyXmlMembershipProvider" type="AspNetReadOnlyXmlMembershipProvider" description="Read-only XML membership provider" xmlFileName="~/App_Data/MembershipUsers.xml"/> 
  </providers> 
</membership>

步骤 2:添加配置条目后,必须将附录中提供的成员资格提供程序代码保存为 %systemdrive%\inetpub\wwwroot\App_Code 目录中的 XmlMembershipProvider.cs。 如果此目录不存在,则必须创建一个。

注意

如果使用记事本,请务必设置“另存为: 所有文件”,以防止文件另存为 XmlMembershipProvider.cs.txt。

步骤 3:处理实际的凭据存储。 在 %systemdrive%\inetpub\wwwroot\App_Data 目录中将以下 XML 代码片段另存为 MembershipUsers.xml 文件。

注意

如果使用记事本,请务必设置“另存为: 所有文件”,以防止文件另存为 MembershipUsers.xml.txt。

<Users>    
    <User>        
        <UserName>Bob</UserName>
        <Password>contoso!</Password>
        <Email>bob@contoso.com</Email>        
    </User>    
    <User>        
        <UserName>Alice</UserName>        
        <Password>contoso!</Password>        
        <Email>alice@contoso.com</Email>        
    </User>    
</Users>

如果 App_Data 目录不存在,则必须创建一个。

注意

由于 Windows Server 2003 和 Windows Vista SP1 中的安全性更改,不能再使用 IIS 管理工具为非 GACed 成员资格提供程序创建成员资格用户帐户。

完成此任务后,转到 IIS 管理工具,并为应用程序添加或删除用户。 从“运行…”菜单中启动“INETMGR”。 打开左侧树状视图中的“+”符号,直到显示“默认网站”。 选择“默认网站”,然后向右移动并单击“安全”类别。 其余功能显示“.NET 用户”。 单击“.NET 用户”,添加一个或多个选中的用户帐户。

在 MembershipUsers.xml 中查找新创建的用户。

创建登录页面

为使用窗体身份验证,我们必须创建登录页面。 打开记事本(为确保可访问以下 wwwroot 目录,必须右键单击 Programs\Accessories\Notepad 图标,然后单击“以管理员身份运行”以管理员身份运行),并在 %systemdrive%\inetpub\wwwroot 目录中创建 login.aspx 文件。 注意 - 务必设置“另存为: 所有文件”,以防止文件另存为 login.aspx.txt。 将以下行粘贴到该文件中:

<%@ Page language="c#" %>    
<form id="Form1" runat="server">    
    <asp:LoginStatus runat="server" />        
    <asp:Login runat="server" />    
</form>

授权规则拒绝访问特定资源时,你将重定向到此登录页面。

测试

打开 Internet Explorer 窗口,请求 http://localhost/default.aspx。 你将看到你重定向到 login.aspx,因为最初你未经过身份验证,并且我们先前拒绝未经身份验证的用户进行访问。 如果使用 MembershipUsers.xml 中指定的用户名/密码对之一成功登录,你将重新重定向到最初请求的 default.aspx 页面。 然后,该页面显示当前时间和你用于身份验证的用户标识。

现在,我们已使用窗体身份验证、登录控件和成员身份成功部署了自定义身份验证解决方案。 此功能在 IIS 7.0 或更高版本中不是新增功能,自早先 IIS 版本的 ASP.NET 2.0 中已经可用。

然而,问题是仅 ASP.NET 处理的内容才受保护。

如果关闭并重新打开浏览器窗口并请求 http://localhost/iisstart.htm,则系统不会提示你输入凭据。 ASP.NET 不参与对静态文件(如 iisstart.htm)的请求。 因此,它无法使用窗体身份验证来对其进行保护。 对于经典 ASP 页面、CGI 程序、PHP 或 Perl 脚本同样如此。 窗体身份验证是 ASP.NET 功能,在请求这些资源期间不可用。

为整个应用程序启用窗体身份验证

在本任务中,我们消除了先前版本中 ASP.NET 的限制,并为整个应用程序启用了 ASP.NET 窗体身份验证和 URL 授权功能。

为利用 ASP.NET 集成,应用程序必须配置为在集成模式下运行。 可对每个应用程序池配置 ASP.NET 集成模式,从而在同一服务器上并行托管不同模式下的 ASP.NET 应用程序。 应用程序所在的默认应用程序池已默认使用集成模式,因此现在无需执行任何操作。

那么,为什么之前尝试访问静态页面时没有体验到集成模式的好处? 答案是 IIS 7.0 及更高版本附带的所有 ASP.NET 模块的默认设置。

利用集成管道

IIS 7.0 及更高版本附带的所有托管模块(包括窗体身份验证和 URL 授权模块)的默认配置使用前提条件,以确保这些模块只应用于 (ASP.NET) 处理程序管理的内容。 这是出于向后兼容的原因。

通过删除前提条件,无论内容如何,我们都可以为对应用程序的所有请求执行所需的托管模块。 为使用基于窗体的身份验证来保护静态文件和任何其他应用程序内容,此操作是必要的。

为此,打开 %systemdrive%\inetpub\wwwroot 目录中的应用程序 web.config 文件,并将以下行粘贴到第一个 <configuration> 元素下方:

<system.webServer> 
<modules> 
    <remove name="FormsAuthenticationModule" />    
    <add name="FormsAuthenticationModule" type="System.Web.Security.FormsAuthenticationModule" />    
    <remove name="UrlAuthorization" />    
    <add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" />    
    <remove name="DefaultAuthentication" />    
    <add name="DefaultAuthentication" type="System.Web.Security.DefaultAuthenticationModule" />    
</modules> 
</system.webServer>

此配置在没有前提条件的情况下重新添加模块元素,从而为对应用程序的所有请求执行这些模块元素。

测试

关闭所有 Internet Explorer 实例,确保不再缓存之前输入的凭据。 打开 Internet Explorer,并向以下 URL 处的应用程序发出请求:

http://localhost/iisstart.htm

你将重定向到 login.aspx 页面以进行登录。

使用先前使用的用户名/密码对进行登录。 成功登录时,你将重新重定向到初始资源,该资源将显示 IIS 欢迎页面。

注意

即使你请求的是静态文件,托管窗体身份验证模块和 URL 授权模块也会提供服务来保护你的资源。

为进一步说明这一点,我们添加了经典 ASP 页面,并使用窗体身份验证对其进行保护。

打开记事本(为确保可访问以下 wwwroot 目录,必须以管理员身份运行 - 右键单击 Programs\Accessories\Notepad 图标,然后单击“以管理员身份运行”),并在 %systemdrive%\inetpub\wwwroot 目录中创建 page.asp 文件。

注意

如果使用记事本,请务必设置“另存为: 所有文件”,以防止文件另存为 page.asp.txt。 将以下行粘贴到其中:

<% 
for each s in Request.ServerVariables
   Response.Write s & ": "&Request.ServerVariables(s) & VbCrLf
next
%>

再次关闭所有 Internet Explorer 实例,否则,凭据仍将被缓存并请求 http://localhost/page.asp。 你将再次重定向到登录页面,并且身份验证成功后,将显示 ASP 页面。

祝贺你 - 你已成功向服务器添加了托管服务,无论处理程序如何,都可以为对服务器的所有请求启用托管服务!

总结

本演练演示了如何利用 ASP.NET 集成模式为 ASP.NET 页面和整个应用程序提供强大的 ASP.NET 功能。

更重要的是,你现在可以使用常见的 ASP.NET 2.0 API 生成新的托管模块,这些 API 能够针对所有应用程序内容运行,并为应用程序提供一组增强的请求处理服务。

请参阅博客 https://www.mvolo.com/,了解在 IIS 7 及更高版本中利用集成模式以及开发利用 ASP.NET 集成的 IIS 模块的更多技巧。 你还可以下载一些此类模块,包括使用 HttpRedirection 模块将请求重定向到应用程序使用 DirectoryListingModule 为 IIS 网站提供美观的目录列表使用 IconHandler 在 ASP.NET 应用程序中显示漂亮的文件图标

附录

此成员资格提供程序基于成员资格提供程序中的示例 XML 成员资格提供程序。

要使用此会员资格提供程序,请在 %systemdrive%\inetpub\wwwroot\App\_Code 目录中将代码另存为 XmlMembershipProvider.cs。 如果此目录不存在,则必须创建一个。 注意 - 如果使用记事本,请务必设置“另存为: 所有文件”,以防止文件另存为 XmlMembershipProvider.cs.txt。

注意

此成员资格提供程序示例仅用于本演示。 它不符合生产成员资格提供程序的最佳做法和安全需求,包括安全地存储密码和审核用户操作。 请勿在应用程序中使用此成员资格提供程序!

using System; 
using System.Xml; 
using System.Collections.Generic; 
using System.Collections.Specialized; 
using System.Configuration.Provider; 
using System.Web.Security; 
using System.Web.Hosting; 
using System.Web.Management; 
using System.Security.Permissions; 
using System.Web; 

public class AspNetReadOnlyXmlMembershipProvider : MembershipProvider 
{ 
    private Dictionary<string, MembershipUser> _Users; 
    private string _XmlFileName; 
                  // MembershipProvider Properties 

    public override string ApplicationName 
    { 
        get { throw new NotSupportedException(); } 
        set { throw new NotSupportedException(); } 
    } 

    public override bool EnablePasswordRetrieval 
    { 
        get { return false; } 
    } 

    public override bool EnablePasswordReset 
    { 
        get { return false; } 
    } 

    public override int MaxInvalidPasswordAttempts 
    { 
        get { throw new NotSupportedException(); } 
    } 

    public override int MinRequiredNonAlphanumericCharacters 
    { 
        get { throw new NotSupportedException(); } 
    } 

    public override int MinRequiredPasswordLength 
    { 
        get { throw new NotSupportedException(); } 
    } 
   
    public override int PasswordAttemptWindow 
    { 
        get { throw new NotSupportedException(); } 
    } 

    public override MembershipPasswordFormat PasswordFormat 
    { 
        get { throw new NotSupportedException(); } 
    } 

    public override string PasswordStrengthRegularExpression 
    { 
        get { throw new NotSupportedException(); } 
    } 
   
    public override bool RequiresQuestionAndAnswer 
    { 
        get { return false; } 
    } 

    public override bool RequiresUniqueEmail 
    { 
        get { throw new NotSupportedException(); } 
    } 
  
   // MembershipProvider Methods 

    public override void Initialize(string name, 
        NameValueCollection config) 
    { 
        // Verify that config isn't null 
        if (config == null) 
            throw new ArgumentNullException("config"); 

        // Assign the provider a default name if it doesn't have one 
        if (String.IsNullOrEmpty(name)) 
            name = "ReadOnlyXmlMembershipProvider"; 
  
        // Add a default "description" attribute to config if the 
        // attribute doesn't exist or is empty 
        if (string.IsNullOrEmpty(config["description"])) 
        { 
            config.Remove("description"); 
            config.Add("description", 
                "Read-only XML membership provider"); 
        } 
  
        // Call the base class's Initialize method 
        base.Initialize(name, config); 
  
        // Initialize _XmlFileName and make sure the path 
        // is app-relative 
        string path = config["xmlFileName"]; 

        if (String.IsNullOrEmpty(path)) 
            path = "~/App_Data/MembershipUsers.xml"; 

        if (!VirtualPathUtility.IsAppRelative(path)) 
            throw new ArgumentException 
                ("xmlFileName must be app-relative"); 

        string fullyQualifiedPath = VirtualPathUtility.Combine 
            (VirtualPathUtility.AppendTrailingSlash 
            (HttpRuntime.AppDomainAppVirtualPath), path); 

        _XmlFileName = HostingEnvironment.MapPath(fullyQualifiedPath); 
        config.Remove("xmlFileName"); 
  
        // Make sure we have permission to read the XML data source and 
        // throw an exception if we don't 
        FileIOPermission permission = 
            new FileIOPermission(FileIOPermissionAccess.Read, 
            _XmlFileName); 
        permission.Demand(); 
  
        // Throw an exception if unrecognized attributes remain 
        if (config.Count > 0) 
        { 
            string attr = config.GetKey(0); 
            if (!String.IsNullOrEmpty(attr)) 
                throw new ProviderException 
                    ("Unrecognized attribute: " + attr); 
        } 
    } 

    public override bool ValidateUser(string username, string password) 
    { 
        // Validate input parameters 
        if (String.IsNullOrEmpty(username) || 
            String.IsNullOrEmpty(password)) 
            return false; 
  
        // Make sure the data source has been loaded 
        ReadMembershipDataStore(); 
  
        // Validate the user name and password 
        MembershipUser user; 

        if (_Users.TryGetValue(username, out user)) 
        { 
            if (user.Comment == password) // Case-sensitive 
            { 
                return true; 
            } 
        } 

        return false; 
    } 
   
    public override MembershipUser GetUser(string username, 
        bool userIsOnline) 
    { 
  
        // Note: This implementation ignores userIsOnline 
        // Validate input parameters 

        if (String.IsNullOrEmpty(username)) 
            return null; 
  
        // Make sure the data source has been loaded 
        ReadMembershipDataStore(); 
  
        // Retrieve the user from the data source 
        MembershipUser user; 

        if (_Users.TryGetValue(username, out user)) 
            return user; 

        return null; 
    } 
   
    public override MembershipUserCollection GetAllUsers(int pageIndex, 
        int pageSize, out int totalRecords) 
    { 
        // Note: This implementation ignores pageIndex and pageSize, 
        // and it doesn't sort the MembershipUser objects returned 
        // Make sure the data source has been loaded 

        ReadMembershipDataStore(); 

        MembershipUserCollection users = 
            new MembershipUserCollection(); 

        foreach (KeyValuePair<string, MembershipUser> pair in _Users) 
            users.Add(pair.Value); 

        totalRecords = users.Count; 

        return users; 
    } 

    public override int GetNumberOfUsersOnline() 
    { 
        throw new NotSupportedException(); 
    } 

    public override bool ChangePassword(string username, 
        string oldPassword, string newPassword) 
    { 
        throw new NotSupportedException(); 
    } 

    public override bool 
        ChangePasswordQuestionAndAnswer(string username, 
        string password, string newPasswordQuestion, 
        string newPasswordAnswer) 
    { 
        throw new NotSupportedException(); 
    } 

    public override MembershipUser CreateUser(string username, 
        string password, string email, string passwordQuestion, 
        string passwordAnswer, bool isApproved, object providerUserKey, 
        out MembershipCreateStatus status) 
    { 
        throw new NotSupportedException(); 
    } 

    public override bool DeleteUser(string username, 
        bool deleteAllRelatedData) 
    { 
        throw new NotSupportedException(); 
    } 

    public override MembershipUserCollection 
        FindUsersByEmail(string emailToMatch, int pageIndex, 
        int pageSize, out int totalRecords) 
    { 
        throw new NotSupportedException(); 
    } 

    public override MembershipUserCollection 
        FindUsersByName(string usernameToMatch, int pageIndex, 
        int pageSize, out int totalRecords) 
    { 
        throw new NotSupportedException(); 
    } 

    public override string GetPassword(string username, string answer) 
    { 
        throw new NotSupportedException(); 
    } 
   
    public override MembershipUser GetUser(object providerUserKey, 
        bool userIsOnline) 
    { 
        throw new NotSupportedException(); 
    } 

    public override string GetUserNameByEmail(string email) 
    { 
        throw new NotSupportedException(); 
    } 
   
    public override string ResetPassword(string username, 
        string answer) 

    { 
        throw new NotSupportedException(); 
    } 
   
    public override bool UnlockUser(string userName) 
    { 
        throw new NotSupportedException(); 
    } 
   
    public override void UpdateUser(MembershipUser user) 
    { 
        throw new NotSupportedException(); 

    } 
   
    // Helper method 

    private void ReadMembershipDataStore() 
    { 
        lock (this) 
        { 
            if (_Users == null) 
            { 
                _Users = new Dictionary<string, MembershipUser> 
                   (16, StringComparer.InvariantCultureIgnoreCase); 
                XmlDocument doc = new XmlDocument(); 
                doc.Load(_XmlFileName); 
                XmlNodeList nodes = doc.GetElementsByTagName("User"); 
  
                foreach (XmlNode node in nodes) 
                { 
                    MembershipUser user = new MembershipUser( 
                        Name,                       // Provider name 
                        node["UserName"].InnerText, // Username 
                        null,                       // providerUserKey 
                        node["Email"].InnerText,    // Email 
                        String.Empty,               // passwordQuestion 
                        node["Password"].InnerText, // Comment 
                        true,                       // isApproved 
                        false,                      // isLockedOut 
                        DateTime.Now,               // creationDate 
                        DateTime.Now,               // lastLoginDate 
                        DateTime.Now,               // lastActivityDate 
                        DateTime.Now,               // lastPasswordChangedDate 
                        new DateTime(1980, 1, 1)    // lastLockoutDate 
                 ); 

                 _Users.Add(user.UserName, user); 

                } 
            } 
        } 
    } 
}