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

来源:互联网 发布:淘宝c店申请企业店铺 编辑:程序博客网 时间:2024/04/28 23:18

目标

实现对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文件一样,貌似挺酷炫的 [呵呵]

0 0