[ASP.NET]利用HttpModule实现动态Web网页内容过滤

目标

实现对Web请求的动态内容进行字符串过滤,比如去掉所有注释和空行(可自行配置),亦可压缩HTML输出流,减小流量消耗。

前言

关于什么是HttpModule及其作用,可自行查找相关文章。本文旨在通过对HttpModule的实际应用,加深对服务器动态处理请求过程的理解。其中参考了网上几位大侠的处理思路,同时结合自身实际,编织出了自己的一套方法。当然肯定有更适合的处理办法,希望有缘能看到这篇文章的朋友们不吝赐教,并且对于其中的不当之处,如能指出,感激不尽。

创建HttpModule

  1. 开始第一步,向项目中添加一个新类HttpFilterModule,实现IHttpModule类的接口,命名为HttpFilterModule.cs,默认位于App_Code文件夹中。
  2. 在IHttpModule中需要实现两个接口函数,一个是Init(HttpApplication application),其中以HttpApplication类型作为传入参数,需要具体实现,也是主要实现方法;一个是Dispose(),无参数,这里无需具体实现方法。HttpFilterModule.cs文件内容如下:
    using System;
    using System.Web;
    using System.Web.Configuration;
    
    /// <summary>
    /// HTTP页面字符串过滤
    /// </summary>
    public class HttpFilterModule : IHttpModule
    {
      public HttpFilterModule(){}
      public void Init(HttpApplication application)
      {
        //TODO: 这里实现具体过滤方法
      }
      public void Dispose() { }
    }
    主要HttpModule建立好了,现在需要的是实现方法。不过现在涉及到另一个很重要的对象:Response.Filter,通过IntelliSense的快速信息我们可以看到,Response.Filter对象属于System.IO.Stream对象,解释是“获取或设置一个包装筛选器对象,该对象用于在传输之前修改HTTP实体主体。”因此可以看出通过修改(设置)改对象可达到过滤HTTP实体主体内容的目的。

创建RawFilter过滤器

既然Response.Filter对象属于System.IO.Stream对象,要修改它就需要建立一个原始过滤对象,继承Stream对象,然后重写Stream对象的Write()方法修改其输出对象,最后将重写后的Stream流对象赋给Response.Filter对象即可。具体操作:新建 RawFilter.cs文件,默认也位于App_Code文件夹中,双击打开输入以下代码:
using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Xml.XPath;

/// <summary>
/// 自定义原始过滤器,用于处理原始数据流
/// </summary>
public class RawFilter : Stream
{
  Stream responseStream;
  HttpRequest request;
  long position;
  StringBuilder responseHtml;

  /// <summary>
  /// 原始过滤器
  /// </summary>
  /// <param name="inputStream">输入流</param>
  public RawFilter(Stream inputStream, HttpRequest httpRequest)
  {
    responseStream = inputStream;
    responseHtml = new StringBuilder();
    request = httpRequest;
  }
  //关键的点,在HttpResponse 输出内容的时候,输出一定会调用此方法数据,所以要在此方法内截获数据(重写)
  public override void Write(byte[] buffer, int offset, int count)
  {
    string strBuffer = System.Text.UTF8Encoding.UTF8.GetString(buffer, offset, count);

    //采用正则表达式,检查输入是否有页面结束符</html>,有即表示页面流输出完毕
    Regex eof = new Regex("</html>", RegexOptions.IgnoreCase);

    if (!eof.IsMatch(strBuffer))
    {
      //页面没有输出完毕,继续追加内容
      responseHtml.Append(strBuffer);
    }
    else
    {
      //页面输出已经完毕,截获内容
      responseHtml.Append(strBuffer);
      string finalHtml = responseHtml.ToString();
      
      //谨慎选择注释以下内容,因为网页内容/排版可能会因此而改变
      //finalHtml = Regex.Replace(finalHtml, "<!--.*-->", string.Empty, RegexOptions.Compiled | RegexOptions.Multiline);
      //finalHtml = Regex.Replace(finalHtml, "^\\s*", string.Empty, RegexOptions.Compiled | RegexOptions.Multiline);
      //finalHtml = Regex.Replace(finalHtml, "\\r\\n", string.Empty, RegexOptions.Compiled | RegexOptions.Multiline);

      //注释以上内容的原因那里把过滤内容写死了,以后手动修改很麻烦,
      //因此我另建FilterString()方法通过读取XML配置文件来动态更新过滤方法
      finalHtml = FilterString(finalHtml); //过滤字符串

      //继续传递要发出的内容写入流
      byte[] data = System.Text.UTF8Encoding.UTF8.GetBytes(finalHtml);

      responseStream.Write(data, 0, data.Length);
    }
  }
  #region 过滤字符串主函数
  /// <summary>
  /// 过滤字符串
  /// </summary>
  /// <param name="InputString">输入源字符串</param>
  /// <returns>返回已过滤的字符串</returns>
  protected string FilterString(string InputString)
  {
    string configPath = HttpContext.Current.Server.MapPath("~/XML/FilterConfig.xml");
    string finalString = InputString;
    try
    {
      if (!System.IO.File.Exists(configPath)) //如果配置文件不存在,则创建默认配置文件
      {
        return finalString; //不存在则原样返回
      }
      else
      {
        XPathDocument xpDoc = new XPathDocument(configPath); //载入只读配置文件
        XPathNavigator xpNav = xpDoc.CreateNavigator();
        XPathNodeIterator xpNt = xpNav.Select("//Rules/FilterRule");

        while (xpNt.MoveNext())
        {
          XPathNavigator xpNav2 = xpNt.Current.Clone();
          string lookFor = ""; //查询规则,为正则表达式
          string sendTo = "";  //重写规则,为正则表达式
          XPathNodeIterator xpNt2 = xpNav2.Select("LookFor");
          while (xpNt2.MoveNext())
          {
            lookFor = xpNt2.Current.Value;
            break;
          }
          xpNt2 = xpNav2.Select("SendTo");
          while (xpNt2.MoveNext())
          {
            sendTo = xpNt2.Current.Value;
            break;
          }
          if (lookFor != string.Empty)
          {
            finalString = Regex.Replace(finalString, lookFor, sendTo, RegexOptions.Compiled | RegexOptions.Multiline);
          }
        } //END WHILE
        return finalString;
      }
    }
    catch (Exception ex)
    {
      return finalString;
    }
  }
  #endregion

  #region 实现 Stream 抽象方法
  public override bool CanRead{get{return true;}}
  public override bool CanSeek{ get { return true; }}
  public override bool CanWrite{ get{ return true; }}
  public override void Close(){ responseStream.Close(); }
  public override void Flush(){ responseStream.Flush(); }
  public override long Length{ get{ return 0; }}
  public override long Position{ get { return position; } set { position = value; }}
  public override int Read(byte[] buffer, int offset, int count){ return responseStream.Read(buffer, offset, count);}
  public override long Seek(long offset, SeekOrigin origin){ return responseStream.Seek(offset, origin); }
  public override void SetLength(long length) { responseStream.SetLength(length);}
  #endregion
}
仔细观察以上代码,整个思路是: 
  • 该类继承Stream类,注意Stream是抽象类,派生类需要实现它的所有抽象方法,因此最下面的抽象方法实现不能遗漏(虽然看似不参与主要功能);
  • 构造函数用于初始化输入流和请求对象;
  • 主要方法Write重写了基类中的方法,首先将字节流转换成字符串,通过正则表达式匹配内容来判断页面输出是否完毕,完毕后再整合原始HTML字符串; 
  • 通过自定义函数对输入HTML字符流进行过滤,主要是在XML配置文件中读取自定义配置节,节点内容是正则表达式的原始匹配字符串和目标字符串,然后进行字符串替换;
  • 最后再将字符串转换成字节数组进行Write输出; 
配置文件 FilterConfig.xml的部分内容如下:
<?xml version="1.0" encoding="utf-8" ?>
<!-- 字符串过滤配置文件 -->

<FilterConfig>
  <!-- 过滤规则 -->
  <Rules>

    <!-- 注释标记 -->
    <FilterRule>
      <LookFor>&lt;!--.*--&gt;</LookFor>
      <SendTo></SendTo>
    </FilterRule>

    <!-- 空行 -->
    <FilterRule>
      <LookFor>^\s*</LookFor>
      <SendTo></SendTo>
    </FilterRule>

  </Rules>
</FilterConfig>
我用正则表达式专门匹配了注释标记和空行,<LookFor>是要匹配的原始字符串,也就是注释标记格式,HTML注释格式为:<!-- --> ,利用正则表达式很方便,这里说个题外话,正则表达式可是个好东西,但是学习起来会有很大的起伏过程,正则表达式测试工具有很多,VS的插件也有,比如RegexTester等,正则表达式很值得探索。<SendTo>配置节内容是匹配之后想要替换成的内容,这里将注释和空行都替换为空字符,也就达到了去掉的目的。

装配RawFilter过滤器

好了,现在要做的就是原始过滤器的装配了,也就是修改(设置)之前的Response.Filter对象。回到 HttpFilterModule.cs文件,其中新建自定义函数 application_ReleaseRequestState用于在HTTP请求结束后进行过滤(关于请求处理过程各个不同状态及不同功能可查阅相关资料,如BeginRequest在刚开始发送请求时发生), application_ReleaseRequestState函数内容如下:
/// <summary>
/// 对此HTTP请求处理的过程全部结束
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void FilterStream(object sender, EventArgs e)
{
  HttpApplication application = (HttpApplication)sender;

  //针对网页类型过滤
  if (application.Response.ContentType.ToLowerInvariant().Contains("text/html"))
  {
    //针对ASPX页面进行拦截
    string reqPath = application.Request.CurrentExecutionFilePath;
    if (reqPath.Contains("aspx"))
    {
      //装配过滤器
      application.Response.Filter = new RawFilter(application.Response.Filter, application.Request);
    }
  }
}
仔细观察以上代码,我只针对后缀名为aspx的文件进行处理,可根据实际情况进行更改。
这样只是完成了一个函数的设计,现在要进行调用,这里就涉及到在什么时候进行调用的问题,关于HttpModule处理请求的不同过程需要了解,这里是要过滤请求完成后的内容,因此需要等待请求结束释放后再进行处理,注意到刚开始建立的 HttpFilterModule.cs文件中的TODO(待完成,备忘)标记了麽,是的,在 HttpFilterModule实现 IHttpModule接口中有个很重要的接口 Init()初始化方法,模块启动时将会调用该方法,其中传入的参数是整个 HttpApplication对象(拿到了这个,还有什么不能干^_^)。因此,在Init()方法中为应用程序对象(HttpApplication)中的释放请求状态事件 ReleaseRequestState添加一个处理事件,也就是 application_ReleaseRequestState处理。
using System;
using System.Web;
using System.Web.Configuration;

/// <summary>
/// HTTP页面字符串过滤
/// </summary>
public class HttpFilterModule : IHttpModule
{
  public HttpFilterModule(){}
  public void Init(HttpApplication application)
  {
    //加入释放请求事件
    application.ReleaseRequestState += new EventHandler(FilterStream);
  }
  public void Dispose() { }
}

配置Web.config文件

整个过程就完成了,捏了一把汗。不过你以为这样就可以成功了麽,那你真是Too young to simple, sometimes naive. 细心地你会问为什么程序知道要执行 HttpFilterModule这个模块呢?问的不错,她真的不知道,所以你要告诉她。其实到了这一步就很简单了,但很关键(很容易忘掉),那就是要在 Web.config文件中添加相应配置,也就是告诉网站程序,我在这里安了一道门,进出都要从我这里过,并且经过我的审核。具体配置节位于 system.web/httpModules中,没有的话需要自行添加。[注:在IIS7及以上版本的服务器中可能需要配置在 system.webServer/Modules配置节中,详情请参考IIS帮助说明]
<system.web>
  <httpModules>
    <add name="HttpFilterModule" type="HttpFilterModule" />
  </httpModules>
</system.web>
注意以上name的内容为模块名,也就是新建的类名,如果有命名空间的要改为“命名空间名.模块名”形式。
以上实现[内容过滤]的问题就完成了,现在可以试试运行了,我们来看看效果。

1. 源代码编辑器中原始代码(含空行和注释)


2. 不加HttpModule模块时运行后查看源代码效果(依然含很多空行和注释)


3. 加上HttpModule模块后运行查看源代码效果(所有空行和注释都已消除)

关于去除注释和空格有什么用处,大家可以自己权衡,我只是提供了一种思路和方法,想法是无穷大的,大家有什么好的创意可以交流交流。比如还可以去除标签外所有空白,将网页压缩成就好像一团乱麻一样(看过百度首页源代码的人应该知道)。效果如下:

就像压缩js文件一样,貌似挺酷炫的 [呵呵]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值