HttpHandler与HttpModule介绍

javascript/jquery

浏览数:296

2019-4-15

前言:作为一个开发人员,我们看过很多的关于开发的书,但是都是教我们”知其然”,并没有教我们”知其所以然”,我们开发web项目的过程中,当我们输完URL敲下回车就跳到我们的网站,我们知道这背后浏览器以及IIS所做的工作嘛?我们只有理解底层的运作原理,才有可能做一个更加优秀的开发人员。
本篇随笔参考了HANS许的 《ASP.NET/MVC/Core的HTTP请求流程》 与张子阳的《HttpHandler介绍》《HttpModule介绍》,双魂人生的《HttpModule和在Global.asax区别》

一、IHttpHandler

我们在开发一个web页面时,大都时候都是去思考页面及处理的逻辑,很少去考虑请求的过程,也就不知道了这个过程里IIS与跳到业务代码之前的代码是怎么跑的。
那我们有没有可能通过代码去操控http请求呢?
答案是肯定的。framework给我们提供了两个主要的接口来实现这一操作,这两个接口就是IHttpHandler与HttpModule,我们在HANDS许的博客中已经知道,ISAPI根据文件名后缀把不同的请求转交给不同
的处理程序,我们打开IIS,点击任意一个网站,再点击”处理程序映射”(IIS低版本下在网站鼠标右击属性,然后选择“主目录”选项,接着选择“配置”),我们能发现大部分的后缀都是通过aspnet_isapi.dll去处理的,但是这个程序不可能对不同的文件采用同一种处理方式,那它是怎么处理的呢?
我们可以打开C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config此目录底下的webconfig文件,我们可以找到以下的代码:

<httpHandlers>
            <add path="eurl.axd" verb="*" type="System.Web.HttpNotFoundHandler" validate="True" />
            <add path="trace.axd" verb="*" type="System.Web.Handlers.TraceHandler" validate="True" />
            <add path="WebResource.axd" verb="GET" type="System.Web.Handlers.AssemblyResourceLoader" validate="True" />
            <add verb="*" path="*_AppService.axd" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="False" />
            <add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="False"/>
            <add path="*.axd" verb="*" type="System.Web.HttpNotFoundHandler" validate="True" />
            <add path="*.aspx" verb="*" type="System.Web.UI.PageHandlerFactory" validate="True" />
            <add path="*.ashx" verb="*" type="System.Web.UI.SimpleHandlerFactory" validate="True" />
            <add path="*.asmx" verb="*" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="False" />
            <add path="*.rem" verb="*" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory, System.Runtime.Remoting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" validate="False" />
            <add path="*.soap" verb="*" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory, System.Runtime.Remoting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" validate="False" />
            <add path="*.asax" verb="*" type="System.Web.HttpForbiddenHandler" validate="True" />
            ......            
</httpHandlers>

在httpHandlers这个节点中,将不同的文件映射给不同的Handler去处理,那我们如果要去操控http请求,我们也可以学着他去实现IHttpHandler接口,写我们所需要的代码。
那我们现在来尝试一下怎么去实现这个需求做一个图片防盗链的例子。

①.因为要实现一个防盗链的程序,我们需要先去处理传过来的http请求,判断是不是我方的,是的话返回正确的图片,不是的话返回一张错误的图片。两张图片如下:
正确图片(一张风景图):

错误的图片(禁止COPY):

所以,我们先建一个专门处理特殊请求的类库HandlerLib,建一个处理后缀名为jpg的请求的类,然后去继承IHttpHandler接口,代码如下。

namespace HandlerLib
{
    /// <summary>
    /// 处理后缀名为jpg的请求
    /// </summary>
    public class JpgHandler : IHttpHandler
    {
        public void ProcessRequest(HttpContext context)
        {
            string FileName = context.Server.MapPath(context.Request.FilePath);
            if (context.Request.UrlReferrer.Host == null)
            {
                context.Response.ContentType = "image/JPEG";
                context.Response.WriteFile("/error.jpg");
            }
            else
            {
                //此处的可填你网站的域名,因为我这里采用的是本机演示,故使用localhost
                if (context.Request.UrlReferrer.Host.IndexOf("localhost") !=-1)
                {
                    context.Response.ContentType = "image/JPEG";
                    context.Response.WriteFile(FileName);
                }
                else
                {
                    context.Response.ContentType = "image/JPEG";
                    context.Response.WriteFile("/error.jpg");
                }
            }
        }
        public bool IsReusable
        {
            get { return true; }
        }
    }
}

写到这里我们就能发现,这不就是一般处理程序嘛,聪明的同学一下子就懂了,平时我们用ajax去请求或实现验证码功能时就是一种特殊的请求,是需要由我们自己来定义的,而不是让框架帮我们去做。
②、接下来,我们学着框架提供的例子到webconfig中的system.web节点下注册一个httpHandlers,那我们这里注册一个后缀名为jpg的。

我们这里新建一个index.aspx页面,放一个img标签来模拟其他网站对我们网站图片的盗用

然后启动网站访问这个页面。访问的页面如下:

我们发现并没有得到我们想要的效果,访问的还是正常的页面。经过去调试,我发现代码也没有跳到那个handler里面,经过查阅相关资料(微软官方MSDN文档),发现框架提供的那种注册写法是针对IIS7以下的,由于我本机是IIS10,所以注册的节点应该在system.webServer。名称也不是HttpHandle而是handler,详情如下图:

此处有一个注意点handler节点比HttpHandle节点多了一个必填属性name,这个属性是由我们自己填的,但不可重复。

我们再次启动index.aspx页面。此次终于达到了我们想要的效果了。如图:

当然,这个例子比较简陋,真正的防盗链有好多种,此例子只是为了帮助我们更好的去理解Http请求的过程中HttpHandler的作用。
写到了这里,我们又去联想,如果多个请求类型能不能写成工厂模式,统一一个入口呢?
我们去查找相关资料,这个时候,我们发现 IHttpHandlerFactory这个接口可以来帮助我们完成这一过程。其定义如下图:

接下来我们进行一个尝试,显然,要实现GetHandler()与ReleaseHandler()方法,微软官方文档上有对其介绍,对此有疑问的同学可移步去学习一下。
我们只要新增一个统一的处理类即可。

  /// <summary>
  /// Handler处理工厂
  /// </summary>
  public  class HandlerFactory : IHttpHandlerFactory
    {
        public IHttpHandler GetHandler(HttpContext context, string requestType, string
       url, string pathTranslated)
        {
            string path = context.Request.PhysicalPath;
            if (Path.GetExtension(path) == ".jpg")
            {
                return new JpgHandler();
            }
            if (Path.GetExtension(path) == ".js")
            {
                return new JsHandler();
            }
            return null;
        }
        public void ReleaseHandler(IHttpHandler handler)
        {
        }
    }

我们查看IIS的处理程序映射,我们发现请求路径的示例中可以用逗号将请求的不同后缀名分隔开,处理程序可为同一个,那我们进行尝试,webconfig的配置为:

    <handlers>
      <add name="factorhandler" path="*.js,*.jpg" verb="*" type="HandlerLib.HandlerFactory" />
    </handlers>

经过尝试,我们发现无效,但是分开定义是没有问题的。

    <handlers>
      <add name="jpghandler" path="*.jpg" verb="*" type="HandlerLib.HandlerFactory"/>
      <add name="jshandler" path="*.js" verb="*" type="HandlerLib.HandlerFactory"/>
    </handlers>

Tips: 如果有清楚IIS的处理程序映射的路径怎么同时设置两种的同学,欢迎在评论区留言指导,本人将不胜感激!

在ASP.NET MVC中也是定义了MVCHandler来做一些MVC特有的动作,举一个最简单的例子,我们要如何判断一个网站是采用ASP.NET还是ASP.NET MVC,最简单的就是打开F12,查看网页的相应头部: 我们可以看到如下页面:

从图中我们便能很清楚知道MVC的版本是什么。

接下来我们去看下源码是如何写的,有需要源码的同学可前往下载(ASP.NET MVC源码),笔者这里是MVC4.0的源码:

其实就是往相应头部增加一个X-AspNetMvc-Version字段,当然这个MVCHandler还做了很多事情,这里就不一一例举了。类似于HTTPHandler的用法还有很多,有兴趣的同学可以自行学习下。

二、IHttpModule

我们在HANS许的博客中知道,在Http请求由IHttpHandler处理之前,它需要通过一系列的HttpModule,在请求处理之后,需要在通过一系列的HttpModule。
与注册HttPHandler类似,我们先看下框架内的示例,同样是C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config此目录底下的webconfig文件(我们给每个module加了注释):

 <httpModules>
        //页面级输出缓存
            <add name="OutputCache" type="System.Web.Caching.OutputCacheModule" />
            //Session 状态管理 
            <add name="Session" type="System.Web.SessionState.SessionStateModule" />
            //用 集 成 Windows身份验证进行客户端验证 
            <add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule" />
            //用基于 Cookie 的窗体身份验证进行客户端身份验证 
            <add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" />
            //用 MS 护照进行客户身份验证 
            <add name="PassportAuthentication" type="System.Web.Security.PassportAuthenticationModule" />
            //管理当前用户角色 
            <add name="RoleManager" type="System.Web.Security.RoleManagerModule" />
            //判断用户是否被授权访问某一URL 
            <add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" />
            //判断用户是否被授权访问某一资源 
            <add name="FileAuthorization" type="System.Web.Security.FileAuthorizationModule" />
            //管理 Asp.Net 应用程序中的匿名访问 
            <add name="AnonymousIdentification" type="System.Web.Security.AnonymousIdentificationModule" />
            //管理用户档案文件的创立 及相关事件 
            <add name="Profile" type="System.Web.Profile.ProfileModule" />
            //捕捉异常,格式化错误提示字符,传递给客户端程序
            <add name="ErrorHandlerModule" type="System.Web.Mobile.ErrorHandlerModule, System.Web.Mobile, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
            //提供类,与服务模型相关。
            <add name="ServiceModel" type="System.ServiceModel.Activation.HttpModule, System.ServiceModel.Activation, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
            //将 URL 请求与定义的路由进行匹配。
            <add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" />
            //管理用于 ASP.NET 中 AJAX 功能的 HTTP 模块。
            <add name="ScriptModule-4.0" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        </httpModules>

接下来我们来看下如何去实现IHttpModule接口:
我们首先看下在元数据中这个接口的定义

很明显,由此定义,当站点第一个资源被访问的时候,Asp.Net 会创建 HttpApplication 类的实例,它代表着站点应用程序,同时会创建所有在 Web.Config 中注册过的 Module 实例。在创建每个Module时去调用Init()方法,这时我们就要在这个方法里注册我们自己的方法

/// <summary>
    /// module
    /// </summary>
    public class ModuleService : IHttpModule
    {
        public ModuleService()
        {
        }

        public string ModuleName
        {
            get { return "ModuleDemo"; }
        }
        
        public void Init(HttpApplication application)
        {
            application.BeginRequest +=(new EventHandler(this.Application_BeginRequest));
            application.EndRequest +=(new EventHandler(this.Application_EndRequest));
        }

        private void Application_BeginRequest(Object source,EventArgs e)
        {
            HttpApplication application = (HttpApplication)source;
            HttpContext context = application.Context;
            context.Response.Write("<h1><font color=red>" +"ModuleDemo: 请求开始" +"</font></h1><hr>");
        }

        private void Application_EndRequest(Object source, EventArgs e)
        {
            HttpApplication application = (HttpApplication)source;
            HttpContext context = application.Context;
            context.Response.Write("<hr><h1><font color=red>" + "ModuleDemo: 结束请求</font></h1>");
        }
      
        public void Dispose() { }
    }

我们新建个页面,如图:

打开浏览器访问此页面,得到此页面:

接下来我们举个”每个页面验证是否登录”的例子:
首先,我们还是在Init方法先注册一个PreRequestHandlerExecute事件,此事件是恰好在 ASP.NET 开始执行事件处理程序(例如,某页或某个 XML Web services)前发生。具体的可以去微软官方MSDN 查看:


然后我们建一个登录的页面,将登录过后的user保存到session。代码如下:
aspx页面:

<form id="form1" runat="server">
        <div style="width:300px;margin: 10% 40%;text-align:right;">
            <div>
                用户名:<input placeholder="请输入用户名" id="user" runat="server" />
            </div>
            <div style="margin-top:20px;">
                <button runat="server" onserverclick="Unnamed_ServerClick">提交</button>
            </div>
        </div>
</form>

aspx.cs页面:

protected void Unnamed_ServerClick(object sender, EventArgs e)
        {
            Session["user"] = user.Value;
            if (Request.QueryString["source"] != null)
            {
                string s = Request.QueryString["source"].ToLower().ToString();   //取出从哪个页面转来的
                Response.Redirect(s); //转到用户想去的页面
            }
            else
            {
                Response.Redirect("index.aspx");    //默认转向index.aspx
            }
        }

通过运行这个例子,我们就能发现,每次请求之前我们都会去查session中是否有user信息,没有的话就回重定向到登录页。
在ASP.NETMVC中,便是定义了一个UrlRoutingModule创建了路由系统(在介绍httpmodule初始,我们已经介绍了这个httpmodule已经在framework中已经默认注册了),从Http请求获取Controller和Action以及路由数据。接着匹配Route规则,获取Route对象,解析对象。UrlRoutingModule这个在System.Web.Routing程序集中,有兴趣的同学可以尝试去反编译下,看看源码。
通过以上的描述,我们也能很清楚的知道httpmodule在整个http请求中的作用。

三 、 HttpModule和Global.asax区别

Global.asax文件我们很熟悉,但是其用法也知道的不多,那我们先来了解下这个文件,再探究下它与HttModule的区别。
global.asax是一个文本文件,它提供全局可用代码。这些代码包括应用程序的事件处理程序以及会话事件、方法和静态变量。有时该文件也被称为应用程序文件。 那其内部方法都有哪些呢,执行顺序是怎样的?
Application_Init 和Application_Start 事件在应用程序第一次启动时被触发一次。相似地,Application_Disposed 和 Application_End 事件在应用程序终止时被触发一次。此外,基于会话的事件(Session_Start 和 Session_End)只在用户进入和离开站点时被使用.其余的事件则处理应用程序请求,这些事件被触发的顺序是:
Application_BeginRequest:在接收到一个应用程序请求时触发。对于一个请求来说,它是第一个被触发的事件,请求一般是用户输入的一个页面请求(URL)。
Application_AuthenticateRequest:在安全模块建立起当前用户的有效的身份时,该事件被触发。在这个时候,用户的凭据将会被验证。
Application_AuthorizeRequest:当安全模块确认一个用户可以访问资源之后,该事件被触发。
Application_ResolveRequestCache:在 ASP.NET 页面框架完成一个授权请求时,该事件被触发。它允许缓存模块从缓存中为请求提供服务,从而绕过事件处理程序的执行。
Application_AcquireRequestState:在 ASP.NET 页面框架得到与当前请求相关的当前状态(Session 状态)时,该事件被触发。
Application_PreRequestHandlerExecute:在 ASP.NET 页面框架开始执行诸如页面或 Web 服务之类的事件处理程序之前,该事件被触发。
Application_PreSendRequestHeaders:在 ASP.NET 页面框架发送 HTTP 头给请求客户(浏览器)时,该事件被触发。
Application_PreSendRequestContent:在 ASP.NET 页面框架发送内容给请求客户(浏览器)时,该事件被触发。
< >
Application_PostRequestHandlerExecute:在 ASP.NET 页面框架结束执行一个事件处理程序时,该事件被触发。
Application_ReleaseRequestState:在 ASP.NET 页面框架执行完所有的事件处理程序时,该事件被触发。这将导致所有的状态模块保存它们当前的状态数据。
Application_UpdateRequestCache:在 ASP.NET 页面框架完成事件处理程序的执行时,该事件被触发,从而使缓存模块存储响应数据,以供响应后续的请求时使用。
Application_EndRequest:针对应用程序请求的最后一个事件。

httpModule事件同Global.asax中的事件相对应,对应关系如下表:

HttpModule Global.asax
BeginRequest Application_BeginRequest
AuthenticateRequest Application_AuthenticateRequest
EndRequest Application_EndRequest

既然global.asax中有的事件httpmodule也大部分都有那我们使用哪个比较好呢?
①、从范围来讲,Global.asax要包括在HttpModule,因为在整个请求的过程中,HttpModule只是从用户发出请求的时候才触发相关,比如服务器启动后要做的事情,它就不能触发,当整个请求都交给httphandler后,那么它就不起作用了,直到httphandler处理完后才可以使用里面的方法,在httphandler处理请求的时候,session是无效的,所以Global.asax可以处理相关的session_end,session_start等事件,而httpMudel却不能,还有服务器停止的时候,需要触发的相关事件等等也只能在Global.asax中触发,还有有些功能,比如统计一个网站的在线人说或者访问量的时候也只能用Global.asax处理,但是共同的范围内还有很多事件可以一起处理的,比如防止sql注入,会在请求的时候过滤一些字符串,这个时候就可以使用它们中的相关事件去处理
②、从个数来说,简单说一下HttpMudel可以多个 Global.asax只有一个,因为对于不同的请求模块,可以创建不同的HttpMudel,而全局应用程序Global.asax却只能有一个
③、可以在应用程序的 Global.asax 文件中实现模块的许多功能,这使您可以响应应用程序事件。但是,模块相对于 Global.asax 文件具有如下优点:模块可以进行封装,因此可以在创建一次后在许多不同的应用程序中使用。通过将它们添加到全局程序集缓存 (GAC) 并将它们注册到 Machine.config 文件中,可以跨应用程序重新使用它们。有关更多信息,但是,使用 Global.asax 文件有一个好处,那就是您可以将代码放在其他已注册的模块事件(如 Session_Start 和 Session_End 方法)中。此外,Global.asax 文件还允许您实例化可在整个应用程序中使用的全局对象。当您需要创建依赖应用程序事件的代码并且希望在其他应用程序中重用模块时,或者不希望将复杂代码放在 Global.asax 文件中时,应当使用模块。当您需要创建依赖应用程序事件的代码但不需要跨应用程序重用它时,或者需要订阅不可用于模块的事件(如 Session_Start)时,应当将代码放在Global.asax 文件中。
总的来说,HttpModule是处理请求的,但是不是全局的,而Global.asax则可以看成是全局的,比如,Application_Start,Session_Start等都可以在这里处理,而不能在HttpModule处理