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

ASP.NET 水印模块

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.55/5 (20投票s)

2005年9月16日

9分钟阅读

viewsIcon

245219

downloadIcon

1839

IHttpModule 的一个实现,用于将水印图像应用于 Web 窗体。

引言

本文介绍了一种通过使用 HTTP 模块将水印图像应用于 ASP.NET Web 窗体的技术。通过自定义 InsertionFilterStream 类的定义,提出了将 HTTP 流过滤作为插入额外标记的方法的概念。为了简化执行此过滤的模块的创建,引入了带有虚拟方法 FilterString 的基类 InsertionModuleBase。最后,定义了 WatermarkerInsertionModule 类作为该基类的一个功能性子类,提供了一种在 Web 窗体和应用程序中插入常见水印图像的实用方法。

背景

2005 年 6 月,CodeProject 成员 Kenny Young[^] 发表了一篇不错的文章,ASP.NET - C# 应用程序环境背景图案[^],其中他演示了一种使用自定义 Page 类作为应用水印图像(Kenny 在他的文章中称之为“背景图案”)的基页的技术。ASP.NET 应用程序中任何显示水印的页面都将继承自他的 BasePage 类。

正如 Kenny 所演示的,在输出 HTML 的 <body> 标签中插入一个简单的 style 属性是创建水印所必需的。我喜欢自动化水印图像的概念,特别是为了区分开发、测试或生产环境,并想知道是否也可以使用 HTTP 模块实现此类功能。(不熟悉 HTTP 模块的读者可以参考 George Shepherd 关于此主题的 MSDN 文章 [^] 作为入门。)这种实现将利用 HTTP 流本身,从而无需从自定义 Page 类派生水印页面。

乍一看,人们可能会尝试直接操作 HttpResponse.OutputStream[^] 属性,以从 HTTP 流中读取并插入所需的 style 属性。毕竟,此属性保存表示传出 HTTP 内容的 Stream 对象,并且 HttpResponse 对象在 HTTP 模块的上下文中肯定是可用的。不幸的是,由于 OutputStream 属性实际上包含一个 HttpResponseStream 类型,它是只写的,因此无法实现这种可能性。尝试从 HttpResponseStream 对象读取会导致抛出异常。

过滤 HTTP 流

允许的是输出流的过滤HttpResponse 对象公开了一个 Filter[^] 属性,它本身是一个 Stream 类型。如果设置了 Filter,则写入响应流的任何输出都将重定向到过滤流。过滤器可以对其内容进行操作,但必须将其结果写回原始流。这有效地形成了两个流的链;实际上,可以应用多个过滤流,前提是每个流都承担将其结果写回它所替换的 Filter 流对象的责任。

我们的过滤流由源下载中包含的 InsertionFilterStream 类定义,其构造函数如下:

public class InsertionFilterStream : Stream
{
  // the original stream we are filtering

  private Stream _originalStream;
 
  // the response encoding, passed in the constructor

  private Encoding _encoding;

  // our string replacement function for inserting text

  private FilterReplacementDelegate _replacementFunction; 

  . . .

  // the constructor must have the original stream for which this one is

  // acting as a filter, the replacement function delegate, and the

  // HttpResponse.ContentEncoding object


  public InsertionFilterStream(Stream originalStream
    , FilterReplacementDelegate replacementFunction
    , Encoding encoding)
  {
    // remember all these objects for later

    _originalStream = originalStream;
    _replacementFunction = replacementFunction;
    _encoding = encoding;
  }

  . . .
}

三个重要信息将传递给我们的过滤对象的构造函数。第一个是我们正在执行字符串操作的原始 Stream 对象。这很可能是 HTTP 输出流,但可能已经存在一个或多个额外的过滤流。实际上,我们将使用 HttpResponse.Filter 属性已经设置的任何流;如果尚未应用其他过滤器,这将是输出流本身。

第二个信息是指向将执行字符串替换的函数的委托。委托的定义具有以下签名:

public delegate string FilterReplacementDelegate(string s);

正如我们稍后将看到的,这为开发人员提供了简单性;开发人员将使用常见的 string 对象,而不是处理编码的字节数组。

构造函数的最后一个参数是一个 Encoding 对象。这也将通过 HttpResponse 对象的 ContentEncoding 属性检索,从而使我们的实现对给定的字符集保持中立。这三个对象存储在 private 类变量中以备后用。

我们过滤 Stream 子类中最重要的方法是 Write 方法。当调用 Write 时,会传入一个字节数组,该字节数组已经根据响应流的字符集进行编码。我们重写的方法执行以下重要步骤:它将编码的字节数组转换为普通的 string 以便于使用,将该字符串传递给替换函数委托,将结果重新编码为字节数组,并将字节写回原始输出流。

public override void Write(byte[] buffer, int offset, int count)
{
    // we want to retrieve the bytes in the buffer array, which are already

    // encoded (using, for example, utf-8 encoding). We'll use the 

    // HttpResponse.ContentEncoding object that was passed in the 

    // constructor to return a string, while accounting for the character

    // encoding of the response stream

    string sBuffer = _encoding.GetString(buffer, offset, count);
 
    // having retrieved the encoded bytes as a normal string, we

    // can execute the replacement function

    string sReplacement = _replacementFunction(sBuffer);
 
    // finally, we have to write back out to our original stream;

    // it is our responsibility to convert the string back to an array of

    // bytes, again using the proper encoding. 

    _originalStream.Write(_encoding.GetBytes(sReplacement)
        , 0, _encoding.GetByteCount(sReplacement));
    
}

基 HTTP 模块

我们的 InsertionFilterStream 类已准备好拦截对输出流的写入,我们将使用 HTTP 模块来挂接到 Web 应用程序的事件并安装我们的过滤器。HTTP 模块是通过实现 IHttpModule[^] 接口创建的,并通过将项添加到 web.configmachine.config 文件的 <system.web> 节的 <httpModules> 子节来安装在 HTTP 管道中。

IHttpModule 契约要求实现者定义两个方法:InitDispose。ASP.NET 运行时将为每个已安装的模块调用 Init,并向每个模块传递 HttpApplication 对象。通过此对象,模块可以为 Web 应用程序公开的任意数量的事件安装处理程序。在我们的例子中,我们只需要挂接到 BeginRequest 事件。

为了支持一个简单的模型来创建依赖字符串替换函数来插入额外标记的模块,我们将定义一个基类,该基类处理与 Web 应用程序的适当事件交互。我们还将包含一个名为 FilterString 的虚拟方法,该方法作为委托发送到 InsertionFilterStream 对象。InsertionModuleBase 类的完整代码如下所示:

public class InsertionModuleBase : IHttpModule
{    
    private FilterReplacementDelegate   _replacementDelegate = null;
    private InsertionFilterStream       _filterStream = null;
 
    public InsertionModuleBase()
    {
    }
 
    // required to support IHttpModule

    public void Dispose()
    {           
    }
     
    public void Init(HttpApplication app)
    {
        // setup an application-level event handler for BeginRequest

        app.BeginRequest 
          += (new EventHandler(this.Application_BeginRequest));          
    }
 
    private void Application_BeginRequest(object source, EventArgs e)
    {
        // upon an application page request, establish our 

        // InsertionFilterStream object in the HttpResponse 

        // filter chain

 
        HttpApplication app = source as HttpApplication;
        if (app != null)
        {
            // construct the delegate function, using the FilterString method;

            // as this method is virtual, it would be overriden in subclasses

            _replacementDelegate = new FilterReplacementDelegate(FilterString);
 
            // construct the filtering stream, taking the existing 

            // HttpResponse.Filter to preserve the Filter chain;

            // we'll also pass in a delegate for our string replacement 

            // function FilterString(), and the character encoding object 

            // used by the http response stream. These will then be used

            // within the custom filter object to perform the string 

            // replacement.

            _filterStream = new InsertionFilterStream(
                                      app.Response.Filter
                                     , _replacementDelegate
                                     , app.Response.ContentEncoding
                            );
 
            // set our filtering stream as the new HttpResponse.Filter 

            app.Response.Filter = _filterStream;
        }
    }

    // This is the function that will be called when it's time to perform

    // string replacement on the web buffer. Subclasses should override this

    // method to define their own string replacements

    protected virtual string FilterString(string s)
    {
        // by default, perform no filtering; just return the given string

        return s;
    }

}

一个简单的插入模块

现在可以通过子类化 InsertionModuleBase 并覆盖 FilterString 方法来创建插入模块。这是一个简单而完整的示例,它将 HTML 输出的 <body> 标签替换为一个带有背景颜色,以及一个显示文本“Development”的标题 <table>

public class SimpleInsertionModule : InsertionModuleBase
{
    protected override string FilterString(string s)
    {
        return s.Replace("<body>"
             , "<body bgcolor='#EFEFEF'>"
             + "  <table width='100%' bgcolor='pink'>"
             + "    <tr><td>Development</td></tr>"
             + "  </table>");
    }
}

如果此类的编译版本(以及支持类 InsertionModuleBaseInsertionFilterStream)可供 Web 应用程序使用(即安装在全局程序集缓存中,或复制到应用程序的 /bin 目录中),则只需简单地修改应用程序的 web.config 文件即可使此字符串替换发生在所有应用程序的 Web 窗体中。或者,可以将此添加内容添加到服务器的 machine.config 文件中,在这种情况下,服务器上的所有应用程序都将具有此功能。

自定义 HTTP 模块通过将项目添加到 web.config<httpModules>[^] 节(嵌套在 <system.web> 中)来包含在 HTTP 管道中。以下格式用于将模块添加到 HTTP 管道

<add type="classname,assemblyname" name="modulename"/>

因此,为了指示包含上面定义的 SimpleInsertionModule,假设命名空间为 MyNamespace,编译成一个名为 InsertionModules.dll 的 .dll,我们可以在 web.configmachine.config 中使用以下片段:

<configuration>
  <system.web>
    <httpModules>
      <add type=”MyNamespace.SimpleInsertionModule,InsertionModules”
           Name=”SimpleInsertionModule” />
    </httpModules>
  </system.web>
</configuration>

WatermarkerInsertionModule 类

为了完成最初的挑战,即使用 HTTP 模块而不是 Page 子类来包含水印图像,我们再次子类化 InsertionModuleBase 并重写 FilterString 方法。除了水印之外,我们还将提供添加页眉和/或页脚的功能。我们还将使用正则表达式进行更强大的模式匹配和不区分大小写的替换。WatermarkerInsertionModule 类的完整代码如下所示:

public class WatermarkerInsertionModule : InsertionModuleBase
{
    protected override string FilterString(string s)
    {
        string sReturn = s;        
 
        // check configuration settings for what the watermarker module 

        // should do;

        string sImage 
     = ConfigurationSettings.AppSettings["WatermarkerInsertionModule_Image"];
        string sTopContents 
     = ConfigurationSettings.AppSettings["WatermarkerInsertionModule_Top"];
        string sBottomContents 
     = ConfigurationSettings.AppSettings["WatermarkerInsertionModule_Bottom"];
 
        // if we want a watermark image, add a <style> tag just before 

        // the </head> tag

        if (sImage != String.Empty && sImage != null)
        {
            Regex rHeadEnd = new Regex("(</head>)", RegexOptions.IgnoreCase);
            
            // this style tag is borrowed directly from Kenny's article

            string sStyle = "<style>body {" 
                          + "background-image: url(" + sImage + "); "
                          + "background-attachment: scroll; "
                          + "background-repeat: repeat; "
                          + "background-color: transparent; "
                          + "}</style>";

            sReturn = rHeadEnd.Replace(sReturn, sStyle + "</head>");
        }
 
        // if we want contents at the top, insert them immediately 

        // after the body tag; we'll use a regular expression to 

        // find the body tag, in case it has other attributes

        if (sTopContents != String.Empty && sTopContents != null)
        {
            Regex rBodyBegin = new Regex("<body.*?>", RegexOptions.IgnoreCase);
            Match m = rBodyBegin.Match(sReturn);
            if (m.Success)
            {
                string matched = m.Groups[0].Value;
                sReturn = sReturn.Replace(matched, matched + sTopContents);
            }
        }
 
        // if we want contents at the bottom, insert them immediately 

        // before the closing </body> tag; again, just to make sure 

        // we're doing this in a case-insensitive way, we'll use a 

        // regular expression to locate it.

        if (sBottomContents != String.Empty && sBottomContents != null)
        {
            Regex rBodyEnd = new Regex("</body>", RegexOptions.IgnoreCase);
            sReturn = rBodyEnd.Replace(sReturn, sBottomContents + "</body>");
        }
 
        // return the filtered string

        return sReturn;
    }
}

与之前的 SimpleInsertionModule 示例一样,WatermarkerInsertionModule 的部署涉及以下步骤:

  1. 将包含 WatermarkerInsertionModuleInsertionFilterStreamInsertionModuleBase 类的编译后的 .dll 安装到全局程序集缓存或 Web 应用程序的 /bin 目录中。
  2. 修改服务器上的 machine.config 文件或 Web 应用程序目录中的 web.config 文件,以在 <system.web><httpModules> 部分中包含适当的 <add> 项。

其他注意事项

尽管将此替换过程归结为自定义子类中单个被覆盖的 FilterString 方法可以为开发人员带来便利,但它并非没有代价。将字节数组转换为字符串再转换回来比直接操作字节数组需要更多的开销。就此而言,使用 HTTP 模块本身就是一种开销。虽然这种技术可能适用于较低流量的开发或测试部署中的水印,但它可能不是在高流量生产站点页面页脚中应用常见版权声明的最佳选择。开发人员必须决定这种技术带来的性能损失是否值得其自身环境的功能。

此外,此技术假设服务器正在缓冲页面输出,因此可以通过一次调用 Write 将完整页面发送到客户端(这是默认的 ASP.NET 行为)。如果禁用页面缓冲(通过将 Page.Response.BufferOutput 设置为 false),此技术可能会产生意想不到的结果。

同样重要的是要注意,当在 <httpModules> 中配置时,模块会接收 ASP.NET 运行时传递给它的所有请求。在某些情况下,例如,一个 .aspx 页面更改内容类型并输出二进制流,或者返回 SOAP 格式消息的 Web 方法,您可能希望禁用该模块,或者至少忽略该请求。您可能希望修改模块源以检查内容类型,或者选择将此类页面放置在自己的子目录中,并包含额外的 web.config 文件,其中在 <httpModules> 部分中使用 <remove> 标签,以便在这些情况下完全禁用该模块。

关于演示项目

演示项目包含文件 InsertionModuleBase.dll,它是本文中提到的所有自定义类的编译版本。主项目目录和子目录中都使用了 Web.config 文件,以配置不同页面选项的 WatermarkerInsertionModule 的包含或排除。

摘要

作为创建基 Page 类的替代方案,WatermarkerInsertionModule 提供了 IHttpModule 接口的实现,用于将水印图像以及常见的页眉和页脚应用于 Web 窗体。它继承自 InsertionModuleBase 类,该类定义了 Web 应用程序 BeginRequest 事件的消费以及 HTTP 输出流自定义过滤器的集成。InsertionFilterStream 拦截对 Write 方法的调用,并应用 WatermarkerInsertionModule 中定义的字符串替换函数。如果开发人员的环境允许页面缓冲以及字符串转换和模块开销所带来的必要性能损失,则 WatermarkerInsertionModule 可以成为在 Web 窗体上执行自动化水印的便捷实用程序。

© . All rights reserved.