65.9K
CodeProject 正在变化。 阅读更多。
Home

使用托管过滤器扩展 IIS 5.x 和 IIS6

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (10投票s)

2007年9月17日

7分钟阅读

viewsIcon

57252

downloadIcon

753

Filter.NET 框架概述,该框架旨在将 ISAPI 过滤器 API 暴露给 .NET,同时保持 IIS 平台的强大、灵活和高效。

引言

从一开始,使用 ISAPI 过滤器扩展 IIS 一直是 C/C++ 专家或低级代码开发人员的工作。随着 ASP.NET 的出现,Microsoft 提供了一个关于“扩展”IIS 的新视角,我双引号括起这个词,因为 HttpModules 是一个包含在 ASP.NET 世界中并受保护的扩展解决方案。虽然 HTTP 模块允许您拦截请求并扩展 ASP.NET 的处理管道,但它们仅影响针对 ASP.NET 框架的请求。

这些 ISAPI 过滤器以及类似的 ISAPI 扩展(以及 IIS 6.0 中的通配符 ISAPI 扩展)是 IIS 作为 Web 服务器可用的扩展槽。能够用 .NET 来构建它们,那就太好了,对吧?并且能够扩展整个 IIS,同时仍然使用 .NET。

我将尝试对一个名为 Filter.NET 的新 .NET 框架进行基本介绍,该框架允许通过用 .NET 构建的 ISAPI 过滤器来扩展 IIS。

IIS7 怎么样?

IIS7 是 Microsoft 对 Web 服务器的统一视图,可以通过 .NET 框架进行扩展和管理。现在不再区分原生扩展和托管扩展。所有扩展槽现在都是托管的,您将能够完全使用 .NET 进行扩展。(准确地说,仍然有一个仅限原生的扩展区域,但希望只在非常特殊的情况下需要)。

然而,这一切仅适用于 IIS7。IIS 5.x 和 IIS 6.0 仍将存在很多年。这就是 Filter.NET 发挥作用的地方。

IIS 事件

与 HTTP 模块类似,IIS 一直有一个事件列表,感兴趣的 ISAPI 过滤器可以订阅这些事件。正是通过这些事件,ISAPI 过滤器才能根据需要检查和修改 IIS 的行为。这些事件在 Filter.NET 中得到了反映,为托管过滤器作者(我通常称之为 .NET 中的 ISAPI 过滤器)提供了与原生过滤器作者相同的工具。

以下事件显示了两个世界(原生和托管)之间的映射:(摘自 MSDN

原生 托管 描述
SF_NOTIFY_READ_RAW_DATA ReadRawData 当从客户端读取数据时发生。每次请求可能发生多次。
SF_NOTIFY_PREPROC_HEADERS PreProcHeaders 在 IIS 预处理完请求头后立即发生,但在 IIS 开始处理请求头内容之前。
SF_NOTIFY_URL_MAP UrlMap 在 IIS 将 URL 转换为服务器上的物理路径后发生。
SF_NOTIFY_AUTHENTICATION 身份验证 在 IIS 身份验证客户端之前发生。
SF_NOTIFY_ACCESS_DENIED AccessDenied 在 IIS 确定对请求的资源访问被拒绝后发生,但在 IIS 将响应发送给客户端之前。
SF_NOTIFY_SEND_RESPONSE SendResponse 在 IIS 处理完请求后发生,但在任何请求头发送回客户端之前。
SF_NOTIFY_SEND_RAW_DATA SendRawData 当 IIS 将原始数据发送回客户端时发生。每次请求可能发生多次。
SF_NOTIFY_END_OF_REQUEST EndOfRequest 在请求结束时发生。
SF_NOTIFY_LOG Log 在请求结束时发生,就在 IIS 将事务写入 IIS 日志之前。
SF_NOTIFY_END_OF_NET_SESSION EndOfNetSession 当与客户端的网络会话结束时发生。
SF_NOTIFY_AUTH_COMPLETE AuthComplete 在与客户端协商完客户端身份后发生。

显示的事件是从原生世界翻译过来的,因为 Filter.NET 本质上是原生框架的托管视图。虽然我可以进一步解释 ISAPI 过滤器的操作和原始细节,但已经有 很好的网站 提供了这些信息。

第一个基本示例

有时,理解工具潜力的最佳方法是看它实际运行。以下示例假设 Filter.NET 已安装并设置。要做到这一点,请按照 CodePlex 此处 概述的步骤进行。

还记得 UpCase 吗?这个旧的 Microsoft ISAPI 过滤器示例是 ISAPI 过滤器强大功能的绝佳体现。想象一下用 .NET 构建它。这正是我在为 Filter.NET 构建新示例时所做的。我在每天构建新示例。

这是你得到的

using System;
using System.Collections.Generic;
using System.Text;
using KodeIT.Web;
 
namespace FilterDotNet.Samples.ChangeCase
{
    internal class Filter : IHttpFilter
    {
        void IHttpFilter.Dispose()
        {
        }
 
        void IHttpFilter.Init(IFilterEvents events)
        {
          events.PreProcHeaders += new EventHandler<PreProcHeadersEventArgs>(
              OnPreProcHeaders);
          events.SendRawData += new EventHandler<RawDataEventArgs>(
              OnSendRawData);
        }
 
        void OnPreProcHeaders(object sender, PreProcHeadersEventArgs e)
        {
            e.Context.Session.Clear();
 
            if (e.Context.Url.ToLower().EndsWith("/uc"))
            {
                e.Context.Session["UpCase"] = true;
                e.Context.Url = e.Context.Url.Substring(0, 
                    e.Context.Url.Length - 3);
            }
            else if (e.Context.Url.ToLower().EndsWith("/lc"))
            {
                e.Context.Session["UpCase"] = false;
                e.Context.Url = e.Context.Url.Substring(0, 
                    e.Context.Url.Length - 3);
            }
        }
 
        void OnSendRawData(object sender, RawDataEventArgs e)
        {
            if (e.Context.Session.ContainsKey("UpCase"))
            {
                bool upCase = (bool)e.Context.Session["UpCase"];
                int streamIndex = 0;
                byte[] bytes = e.Context.GetData();
 
                if (!e.Context.Session.ContainsKey("Headers"))
                {
                    for (int ix = 0; ix < (bytes.Length - 2); ix++)
                    {
                        if ((bytes[ix + 0] == 0x0D) && (
                            bytes[ix + 1] == 0x0A) &&

                            (bytes[ix + 2] == 0x0D) && (
                                bytes[ix + 3] == 0x0A))
                        {
                            e.Context.Session["Headers"] = null;
                            streamIndex = ix + 4;
                            break;
                        }
                    }
                }
 
                if (e.Context.Session.ContainsKey("Headers"))
                {
                    if (upCase)
                    {
                        for (; streamIndex < bytes.Length; streamIndex++)
                        {
                            byte b = bytes[streamIndex];
                            if ((b >= 'a') && (b <= 'z'))
                            {
                                bytes[streamIndex] = (byte)((b - 'a') + 'A');
                            }
                        }
                    }
                    else
                    {
                        for (; streamIndex < bytes.Length; streamIndex++)
                        {
                            byte b = bytes[streamIndex];
                            if ((b >= 'A') && (b <= 'Z'))
                            {
                                bytes[streamIndex] = (byte)((b - 'A') + 'a');
                            }
                        }
                    }
 
                    e.Context.SetData(bytes);
                }
            }
        }
    }
}

从代码上看,你会发现与 HttpModules 类似,有一个 IHttpFilter 接口。由于 IIS 是基于事件的,因此基于事件订阅工作类似的扩展模板是有意义的。

public interface IHttpFilter
{
    void Init(IFilterEvents events);
    void Dispose();
}

是不是很熟悉?这就是重点。可供订阅的事件是上表中列出的事件。每个事件中的操作同样反映了原生模式已为我们提供的功能。

仔细查看代码,我们可以看到 SendRawData 事件在开始转换过程之前会找到 HTTP 请求头的末尾。PreProcHeaders 事件用于指示转换是转换为大写、小写还是不进行任何转换。之后,如有必要,URL 会被调整。

第二个基本示例

第二个示例非常有趣。它源于我的一位朋友在谈论使用基本身份验证时,担心即使有 SSL,也无法阻止身份验证凭据因超时而被限制。考虑到这一点,我想到了为这种“开箱即用”的身份验证模式“创建”一个超时行为,就像我们通常使用自定义身份验证那样。

虽然这可以通过托管模块(又名 HttpModules)来实现,但它仅影响 ASP.NET 应用程序,而您的环境可能包含其他语言编写的应用程序,如 ASP、PHP 等。在这些情况下,您需要一种适用于您发现敏感的每个页面的解决方案。

继续这个例子,IIS 身份验证选项假定为“基本身份验证”。考虑到这一点,解决方案的要求是:

  • 等待用户通过基本身份验证进行身份验证。
  • 在用户身份验证后记录 TCP 会话的开始。
  • 每次请求时,记录最后访问日期。
  • 每次请求时,检查空闲时间是否超过某个限制。如果超过,则再次要求用户提供凭据。由于 401 响应将结束当前的 TCP 会话,因此当我们再次发送凭据时,用户一定会启动一个新的 TCP 会话。

基于以上要求,使用托管过滤器(又名 HttpFilters)非常简单。我们感兴趣的事件是:

  • 当请求首次到达 IIS 时(PreProcHeaders 事件)。
  • 当基本身份验证凭据即将由 IIS 验证时(Authentication 事件)。
  • 当访问被 IIS 拒绝时(AccessDenied 事件)。

假设我们将会话空闲超时设置在某个地方(例如配置文件),那么一切就绪。让我们构建托管过滤器。

using System;
using System.Text;
using KodeIT.Web;
 
namespace MyFilter
{
    public class Class1 : IHttpFilter
    {
        const string SESSION_LOGGEDIN = "LoggedIn";
 
        public void Dispose() { }
 
        public void Init(IFilterEvents events)
        {
            events.PreProcHeaders += new EventHandler(OnPreProcHeaders);
            events.Authentication += new EventHandler(OnAuthentication);
            events.AccessDenied += new EventHandler(OnAccessDenied);
        }
 
        void OnAccessDenied(object sender, AccessDeniedEventArgs e)
        {
            e.Context.Session.Remove(SESSION_LOGGEDIN);
        }
 
        void OnAuthentication(object sender, AuthenticationEventArgs e)
        {
            if (e.Context.ServerVariables[
                ServerVariable.AUTH_TYPE].ToLower().Equals("basic"))
            {
                e.Context.Session[SESSION_LOGGEDIN] = DateTime.Now;
            }
        }
 
        void OnPreProcHeaders(object sender, PreProcHeadersEventArgs e)
        {
            if (e.Context.Session.ContainsKey(SESSION_LOGGEDIN))
            {
                if ((DateTime.Now - (DateTime)e.Context.Session[
                    SESSION_LOGGEDIN]) > new TimeSpan(0, 0, 10))
                {
                    string response = String.Format(
                        "HTTP/1.0 401 Unauthorised\r\nWWW-Authenticate:" + 
                        "Basic\r\n\r\n");
                    e.Context.WriteClient(ASCIIEncoding.ASCII.GetBytes(
                        response));
                    e.Context.TerminateRequest(false);
                }
                else
                {
                    e.Context.Session[SESSION_LOGGEDIN] = DateTime.Now;
                }
            }
        }
    }
}

您可能想知道 e.Context.Session 集合及其用途。此集合是与 TCP 会话关联的一个属性包。每当启动 TCP 会话时(来自浏览器的第一个请求到达 IIS),都会创建属性包。此属性包实例将在 TCP 会话期间保持活动状态。当 TCP 会话终止时(由 EndOfNetSession 事件处理),会话实例也会被销毁。

回到代码,您在此处看到的 TimeSpan(0, 0, 10) 可以在配置文件中设置。在此示例中,其值为 10 秒,这意味着如果您在 10 秒内没有发出新的 HTTP 请求,您将被再次要求提供凭据。

设置 Filter.NET

要设置 Filter.NET,请遵循以下简单步骤:

  • 此处 或文章顶部下载 Filter.NET。
  • 运行 filterdotnetfx.msi 进行安装。
  • 通过运行以下命令在 IIS 中注册 Filter.NET:
C:\WINDOWS\Filter.NET\v1.0.1>filter_regiis.exe -i
ACL: Adding (R)ead, (E)xecute for IIS_WPG in C:\WINDOWS\Filter.NET\v1.0.1 ...
     Account IIS_WPG does not exist, and is ignored.
ACL: Adding (R)ead, (E)xecute for ASPNET in C:\WINDOWS\Filter.NET\v1.0.1 ... 
     Done
IIS: Add ISAPI Filter Filter.NET_v1.0.1 to /LM/W3SVC ... done
IIS: Wait 30s for IIS to stop ... stopped
IIS: Wait 30s for IIS to start ... running

完成此步骤后,Filter.NET 已安装在 IIS 中。

配置托管过滤器

Filter.NET 有一个简单的配置文件,位于 %SYSTEMROOT%\Filter.NET\[version]\bin\filter.config。此文件所在的目录也是预期放置所有托管过滤器(除了 GAC)的目录。

配置文件格式为:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  
  <configSections>
    <section name="httpFilters"
        type="KodeIT.Web.Configuration.HttpFiltersSection, KodeIT.Web, 
        Version=1.0.1.0, Culture=neutral, 
        PublicKeyToken=18823f5c6a796933" />
  </configSections>
 
  <httpFilters>

  <!-- 
      Add here the managed filters
      
      <add name="myManagedFilter" type="MyNamespace.MyClass, MyAssembly" />
  -->
  </httpFilters>
 
</configuration>

<httpFilters> 标签包含一些有用的属性 — errorDetailerrorPage — 用于定义何时显示错误页面以及错误页面应是什么样子。可用的可选属性是:

<httpFilters errorDetail="On|Off|LocalOnly" errorPage="somePage.htm">
</httpFilters>

如上面的代码片段所示,您可以选择显示带或不带详细信息的错误。您也可以仅在请求来自 IIS 所在本地机器时显示它们。

我们目前的进展

Filter.NET 是我在 Microsoft .NET 对 IIS 5x 和 IIS 6.0 的整体方法中发现缺失的框架之一。在创建了几个 ISAPI 过滤器之后,考虑到 .NET 框架的实用性,我想到了创建这个框架。在性能方面,它不如 C/C++ 快,但你会发现它的速度几乎一样快。开发上的收益很明显,所以我知道这是一个不错的选择!

此框架也可在 此处 获取。有几个示例可用,我会尽量构建更多示例。也欢迎您自己构建更多示例。

我将通过 博客 来讨论这个问题。

尽情享用!

© . All rights reserved.