asp.net httpmodule and httphander

ASP.NET中的HTTP模块和处理程序

     介绍
  
    在Internet时代的开端,客户端的需求非常有限;.htm文件就可以满足他们的需求。但是,随着时间的流逝,客户端需求的扩充超越了.htm文件或静态文件所包含的功能。
  
  开发者需要扩充或扩展Web服务器的功能。Web服务器厂商设计了不同的解决方案,但是都遵循同一个主题“向Web服务器插入某些组件”。所有的Web服务器补充技术都允许开发者建立并插入组件以增强Web服务器的功能。微软公司提出了ISAPI(Internet服务器API),网景公司提出了NSAPI(网景服务器API)等等。
  
    ISAPI是一种重要的技术,它允许我们增强与ISAPI兼容的Web服务器(IIS就是一种与ISAPI兼容的Web服务器)的能力。我们使用下面的组件达到这个目的:
  
    · ISAPI扩展
  
    · ISAPI过滤器
  
    ISAPI扩展是使用Win32动态链接库来实现的。你可以把ISAPI扩展看作是一个普通的应用程序。ISAPI扩展的处理目标是http请求。这意味着你必须调用它们才能激活它们。 你可以认为ISAPI过滤器仅仅就是一个过滤器而已。客户端每次向服务器发出请求的时候,请求要经过过滤器。客户端不需要在请求中指定过滤器,只需要简单地把请求发送给Web服务器,接着Web服务器把请求传递给相关的过滤器。接下来过滤器可能修改请求,执行某些登录操作等等。
  
    由于这些组件的复杂性,实现它们非常困难。开发者不得不使用C/C++来开发这些组件,但是对于很多人来说,使用C/C++进行开发简直就是痛苦的代名词。
  
    那么ASP.NET提供什么东西来实现这些功能呢?ASP.NET提供的是HttpHandler(HTTP处理程序)和HttpModule(HTTP模块)。
  
    在深入了解这些组件的详细信息之前,了解一下http请求经过HTTP模块和HTTP处理程序的时候的处理流程是有价值的。
  
    建立示例应用程序
  
    我建立了下面一些的C#项目以演示应用程序的不同组件:
  
    · NewHandler (HTTP处理程序)
  
    · Webapp (演示HTTP处理程序)
  
    · SecurityModules (HTTP模块)
  
    · Webapp2 (演示HTTP模块)
  
    这些应用程序的安装步骤:
  
    · 解开attached zip文件中的所以代码。
  
    · 建立两个虚拟目录webapp和webapp2;把这两个目录指向Webapp和Webapp2应用程序的实际物理目录。
  
    · 把NewHandler项目中的Newhandler.dll文件复制到webapp应用程序的bin目录。
  
    · 把SecurityModules项目中的SecurityModules.dll文件复制到webapp2应用程序的bin目录中。 
  ASP.NET请求的处理过程
  
    ASP.NET请求处理过程是基于管道模型的,在模型中ASP.NET把http请求传递给管道中的所有模块。每个模块都接收http请求并有完全控制权限。模块可以用任何自认为适合的方式来处理请求。一旦请求经过了所有HTTP模块,就最终被HTTP处理程序处理。HTTP处理程序对请求进行一些处理,并且结果将再次经过管道中的HTTP模块:
  
  请注意在http请求的处理过程中,只能调用一个HTTP处理程序,然而可以调用多个HTTP模块。
  
    Http处理程序
  
    HTTP处理程序是实现了System.Web.IHttpHandler接口的.NET组件。任何实现了IHttpHandler接口的类都可以用于处理输入的HTTP请求。HTTP处理程序与ISAPI扩展有些类似。HTTP处理程序和ISAPI扩展的差别在于在URL中可以使用HTTP处理程序的文件名称直接调用它们,与ISAPI扩展类似。
  
    HTTP处理程序实现了下列方法:
  
  方法名称 描述
  ProcessRequest 这个方法实际上是http处理程序的核心。我们调用这个方法来处理http请求。
  IsReusable 我们调用这个属性来决定http处理程序的实例是否可以用于处理相同其它类型的请求。HTTP处理程序可以返回true或false来表明它们是否可以重复使用。
  
    你可以使用web.config或者machine.config文件把这些类映射到http请求上。映射完成以后,当接收到相应请求的时候ASP.NET会实例化http处理程序。我们将解释如何在web.config和/或machine.config文件中定义所有这些细节信息。
  
    ASP.NET还通过IHttpHandlerFactory接口支持http处理程序的扩展。ASP.NET提供了把http请求路由到实现IHttpHandlerFactory接口的类的对象上的能力。此外,ASP.NET还利用了Factory设计模式。这种模式为建立一组相关对象而不提供具体类的功能提供了接口。简单的说,你可以把用于建立依赖传递进来的参数建立的http处理程序对象的类看作是factory(工厂)。我们不用指定需要实例化的特定的http处理程序;http处理程序工厂处理这种事务。这样做的优点在于如果未来实现IHttpHandler接口的对象的实现方法发生了改变,只要接口仍然相同,客户端就不会受到影响。
  
    下面是IHttpHandlerFactory接口中的方法列表:
  
  方法名称 描述
  GetHandler 这个方法负责建立适当的处理程序并把它的指针返回到调用代码(ASP.NET运行时)。这个方法返回的处理程序对象应该实现了IHttpHandler接口。
  ReleaseHandler 这个方法负责在请求处理完成后释放http处理程序。Factory 实现决定了它的操作。Factory 实现可以是实际摧毁实例,也可以把它放入缓冲池供以后使用。
  
    在配置文件中注册HTTP处理程序和HTTP处理程序工厂
  
    ASP.NET在下面的配置文件中维护自己的配置信息:
  
    · machine.config
  
    · web.config
  
    machine.config文件包含应用于计算机上安装的所有Web应用程序的配置设置信息。
  
    web.config文件对于每个Web应用程序来说是特定的。每个Web应用程序都有自己的web.config文件。Web应用程序的任何子目录也可能包含自己的web.config文件;这使得它们能够覆盖父目录的设置信息。
  为了给我们的Web应用程序添加HTTP处理程序,你可以使用<httpHandlers>和<add>节点。实际上,处理程序都带有<add>节点,列举在<httpHandlers>和</httpHandlers>节点之间。下面是添加HTTP处理程序的一个普通的例子:
  
  <httpHandlers>
   <add verb="supported http verbs" path="path" type="namespace.classname, assemblyname" />
  <httpHandlers>
  
    在上面的XML中,
  
    · Verb属性指定了处理程序支持的HTTP动作。如果某个处理程序支持所有的HTTP动作,请使用“*”,否则使用逗号分隔的列表列出支持的动作。因此如果你的处理程序只支持HTTP GET和POST,那么verb属性就应该是“GET, POST”。
  
    · Path属性指定了需要调用处理程序的路径和文件名(可以包含通配符)。例如,如果你希望自己的处理程序只有在test.xyz文件被请求的时候才被调用,那么path属性就包含“test.xyz”,如果你希望含有.xyz后缀的所有文件都调用处理程序,path属性应该包含“*.xyz”。
  
    · Type属性用名字空间、类名称和部件名称的组合形式指定处理程序或处理程序工厂的实际类型。ASP.NET运行时首先搜索应用程序的bin目录中的部件DLL,接着在全局部件缓冲(GAC)中搜索。 
 ASP.NET运行时对HTTP处理程序的使用方式
  
    无论你是否相信,ASP.NET都使用HTTP请求实现了大量的自己的功能。ASP.NET使用处理程序来处理.aspx、 .asmx、 .soap和其它ASP.NET文件。
  
    下面是machine.config文件中的一个片段:
  
  <httpHandlers>
   <add verb="*" path="trace.axd" type="System.Web.Handlers.TraceHandler"/>
   <add verb="*" path="*.aspx" type="System.Web.UI.PageHandlerFactory"/>
   <add verb="*" path="*.ashx" type="System.Web.UI.SimpleHandlerFactory"/>
   <add verb="*" path="*.config" type="System.Web.HttpForbiddenHandler"/>
   <add verb="GET,HEAD" path="*" type="System.Web.StaticFileHandler"/>
   . . . . . .
   . . . . . .
  </httpHandlers>
  
    在上面的配置信息中你可以看到对.aspx文件的所有请求都由System.Web.UI.PageHandlerFactory类来处理。与此类似,对.config文件和其它文件(它们不能被客户端直接访问)的所有请求都由System.Web.HttpForbiddenHandler类处理。你可能已经猜到,当访问这些文件的时候,该类简单地给客户端返回一个错误信息。
  
    执行HTTP处理程序
  
    现在你将看到如何实现一个HTTP处理程序。那么我们的新处理程序要做什么任务呢?前面我提到,处理程序大多数用于给Web服务器添加新功能;因此,我将建立一个处理程序来处理新的文件类型——扩展名为.15seconds的文件。我们建立了这个处理程序并在我们的Web应用程序的web.config文件中注册之后,所有对.15seconds文件的请求都将由这个新处理程序来处理。
  
    你可能正在考虑这个处理程序的使用方法。如果你希望引入一种新的服务器脚本语言或动态服务器文件(例如asp、aspx)该怎么办呢?你可以为它编写一个自己的处理程序。类似地,如果你希望在IIS上运行Java小程序、JSP和其它一些服务器端Java组件应该怎么办呢?一种方法是安装某些ISAPI扩展(例如Allaire或Macromedia Jrun)。你也可以编写自己的HTTP处理程序。尽管这对于第三方厂商(例如Allaire和Macromedia)来说是很复杂的事务,但是它却是个很有吸引力的选择,因为它们的HTTP处理能够能够访问ASP.NET运行时暴露的所有新功能。
  
    实现我们的HTTP处理程序包含以下步骤:
  
    1.编写一个实现IHttpHandler接口的类。
  
    2. 在web.config或machine.config文件中注册这个处理程序。
  
    3.在Internet服务管理器中把文件扩展(.15seconds)映射到ASP.NET ISAPI扩展DLL(aspnet_isapi.dll)上。
  
    第一步
  
    在Visual Studio.NET中建立一个新的C#类库项目,并把它命名为“MyHandler”。Visual Studio.NET将自动地给项目添加一个叫做“Class1.cs”的类。把它改名为“NewHandler”;在代码窗口中打开这个类,并把类的名称和构造函数的名称改成“NewHandler”。
  
    下面是NewHandler类的代码:
  
  using System;
  using System.Web;
  
  namespace MyHandler
  {
   public class NewHandler : IHttpHandler
   {
    public NewHandler()
    {
     // TODO: 此处添加构造逻辑
    }
  
    #region Implementation of IHttpHandler
    public void ProcessRequest(System.Web.HttpContext context)
    {
     HttpResponse objResponse = context.Response ;
     objResponse.Write("<html><body><h1>Hello 15Seconds Reader ") ;
     objResponse.Write("</body></html>") ;
    }
  
    public bool IsReusable
    {
     get
     {
      return true;
     }
    }
    #endregion
   }
  }
  
    你在ProcessRequest方法中可以看到,该HTTP处理程序通过System.Web.HttpContext对象访问了所有作为参数传递给它的ASP.NET内部对象。实现ProcessRequest方法只需要简单地从context对象中提取HttpResponse对象并把发送一些HTML给客户端。类似地,IsReusable返回true,表明这个处理程序可以被重复用作处理其它的HTTP请求。
  
    我们编译上面的代码并把它放到webapp虚拟目录的bin目录之中。
  
    第二步
  
    在web.config文件中通过添加下面的文本来注册这个处理程序:
  
  <httpHandlers>
   <add verb="*" path="*.15seconds" type="MyHandler.NewHandler,MyHandler"/>
  </httpHandlers>
  
    第三步
  
    由于我们已经建立了用于处理新扩展文件的处理程序了,我们还需要把这个扩展名告诉IIS并把它映射到ASP.NET。如果你不执行这个步骤而试图访问Hello.15seconds文件,IIS将简单地返回该文件而不是把它传递给ASP.NET运行时。其结果是该HTTP处理程序不会被调用。
  
    运行Internet服务管理器,右键点击默认Web站点,选择属性,移动到Home目录选项页,并点击配置按钮。应用程序配置对话框弹出来了。点击添加按钮并在可执行字段输入aspnet_isapi.dll文件路径,在扩展字段输入.15seconds。其它字段不用处理;该对话框如下所示:
  
  
  
  点击确认按钮关闭应用程序配置和默认Web站点属性对话框。
  
    现在我们运行Internet Explorer并输入url:http://localhost/webapp/hello.15seconds,看到的页面如下:
  
HTTP处理程序中的对话状态
  
    维护对话状态是Web应用程序执行的最通常的事务。HTTP处理程序也需要访问这些对话状态。但是HTTP处理程序的默认设置是没有激活对话状态的。为了读取和/或写入状态数据,需要HTTP处理程序实现下面的接口之一:
  
    · IRequiresSessionState
  
    · IReadOnlySessionState.
  
    当HTTP处理程序需要读写对话数据的时候,它必须实现IRequiresSessionState接口。如果它只读取对话数据,实现IReadOnlySessionState接口就可以了。
  
    这两个接口都是标记接口,并没有包含任何方法。因此,如果你希望激活NewHandler处理程序的对话状态,要像下面的代码一样声明NewHandler类:
  
  public class NewHandler : IHttpHandler, IRequiresSessionState
  
    HTTP模块
  
    HTTP模块是实现了System.Web.IhttpModule接口的.NET组件。这些组件通过在某些事件中注册自身,把自己插入ASP.NET请求处理管道。当这些事件发生的时候,ASP.NET调用对请求有兴趣的HTTP模块,这样该模块就能处理请求了。
  
    HTTP模块实现了IhttpModule接口的下面一些方法:
  
  方法名称 描述
  Init 这个方法允许HTTP模块向HttpApplication 对象中的事件注册自己的事件处理程序。
  Dispose 这个方法给予HTTP模块在对象被垃圾收集之前执行清理的机会。
  
    HTTP模块可以向System.Web.HttpApplication对象暴露的下面一些方法注册:
  
  事件名称 描述
  AcquireRequestState 当ASP.NET运行时准备好接收当前HTTP请求的对话状态的时候引发这个事件。
  AuthenticateRequest 当ASP.NET 运行时准备验证用户身份的时候引发这个事件。
  AuthorizeRequest 当ASP.NET运行时准备授权用户访问资源的时候引发这个事件。
  BeginRequest 当ASP.NET运行时接收到新的HTTP请求的时候引发这个事件。
  Disposed 当ASP.NET完成HTTP请求的处理过程时引发这个事件。
  EndRequest 把响应内容发送到客户端之前引发这个事件。
  Error 在处理HTTP请求的过程中出现未处理异常的时候引发这个事件。
  PostRequestHandlerExecute 在HTTP处理程序结束执行的时候引发这个事件。
  PreRequestHandlerExecute 在ASP.NET开始执行HTTP请求的处理程序之前引发这个事件。在这个事件之后,ASP.NET 把该请求转发给适当的HTTP处理程序。
  PreSendRequestContent 在ASP.NET把响应内容发送到客户端之前引发这个事件。这个事件允许我们在内容到达客户端之前改变响应内容。我们可以使用这个事件给页面输出添加用于所有页面的内容。例如通用菜单、头信息或脚信息。
  PreSendRequestHeaders 在ASP.NET把HTTP响应头信息发送给客户端之前引发这个事件。在头信息到达客户端之前,这个事件允许我们改变它的内容。我们可以使用这个事件在头信息中添加cookie和自定义数据。
  ReleaseRequestState 当ASP.NET结束所搜有的请求处理程序执行的时候引发这个事件。
  ResolveRequestCache 我们引发这个事件来决定是否可以使用从输出缓冲返回的内容来结束请求。这依赖于Web应用程序的输出缓冲时怎样设置的。
  UpdateRequestCache 当ASP.NET完成了当前的HTTP请求的处理,并且输出内容已经准备好添加给输出缓冲的时候,引发这个事件。这依赖于Web应用程序的输出缓冲是如何设置的。
  
    除了这些事件之外,我们还可以使用四个事件。我们可以通过实现Web应用程序的global.asax文件中一些方法来使用这些事件。
  
    这些事件是:
  
    · Application_OnStart
  
    当第一个请求到达Web应用程序的时候引发这个事件。
  
    · Application_OnEnd
  
    准备终止应用程序之前引发这个事件。
  
    · Session_OnStart
  
    用户对话的第一个请求引发这个事件。
  
    · Session_OnEnd
  
    放弃对话或者对话超期的时候引发这个事件。
在配置文件中注册HTTP模块
  
    当我们建立了HTTP模块并把它复制到Web应用程序的bin目录或者全局部件缓冲(Global Assembly Cache)之后,接下来就应该在web.config或machine.config中注册它了。
  
    我们可以使用<httpModules>和<add>节点把HTTP模块添加到Web应用程序中。实际上模块都使用<add>节点列举在<httpModules>和</httpModules>节点之内了。
  
    因为配置设置信息是可以继承的,所以子目录从父目录那儿继承配置设置信息。其结果是,子目录可能继承了一些不需要的HTTP模块(它们是父配置信息的一部分);因此,我们需要一种删除这些不需要的模块的方法。我们可以使用<remove>节点;如果我们希望删除从应用程序继承得到的所有HTTP模块,可以使用<clear>节点。
  
    下面的代码是添加HTTP模块的一个通用示例:
  
  <httpModules>
  <add type="classname, assemblyname" name="modulename" />
  <httpModules>
  
    下面的代码是从应用程序中删除HTTP模块的一个通用示例:
  
  <httpModules>
  <remove name="modulename" />
  <httpModules>
  
    在上面的XML中:
  
    · Type属性用类和部件名称的形式指定了HTTP模块的实际类型。
  
    · Name属性指定了模块的友好名称。其它应用程序可以使用这个名称来识别HTTP模块。
  
    ASP.NET运行时如何使用HTTP模块
  
    ASP.NET运行时使用HTTP模块实现某些特殊的功能。下面的片段来自于machine.config文件,它显示了ASP.NET运行时安装的HTTP模块:
  
  <httpModules>
   <add name="OutputCache" type="System.Web.Caching.OutputCacheModule"/>
   <add name="Session" type="System.Web.SessionState.SessionStateModule"/>
   <add name="WindowsAuthentication"
  type="System.Web.Security.WindowsAuthenticationModule"/>
   <add name="FormsAuthentication"
  type="System.Web.Security.FormsAuthenticationModule"/>
   <add name="PassportAuthentication"
  type="System.Web.Security.PassportAuthenticationModule"/>
   <add name="UrlAuthorization"
  type="System.Web.Security.UrlAuthorizationModule"/>
   <add name="FileAuthorization"
  type="System.Web.Security.FileAuthorizationModule"/>
  </httpModules>
  
    ASP.NET使用上面一些HTTP模块来提供一些服务,例如身份验证和授权、对话管理和输出缓冲。由于这些模块都注册在machine.config文件中。 
 实现一个提供安全服务的HTTP模块
  
    现在我们实现一个HTTP模块,它为我们的Web应用程序提供安全服务。该HTTP模块基本上是提供一种定制的身份认证服务。它将接收HTTP请求中的身份凭证,并确定该凭证是否有效。如果有效,与用户相关的角色是什么?通过User.Identity对象,它把这些角色与访问我们的Web应用程序页面的用户的标识关联起来。
  下面是该HTTP模块的代码:
  
  using System;
  using System.Web;
  using System.Security.Principal;
  
  namespace SecurityModules
  {
   /// Class1的总体描述。
  
   public class CustomAuthenticationModule : IHttpModule
   {
    public CustomAuthenticationModule()
    {
    }
    public void Init(HttpApplication r_objApplication)
    {
     // 向Application 对象注册事件处理程序。
     r_objApplication.AuthenticateRequest +=
  new EventHandler(this.AuthenticateRequest) ;
    }
  
    public void Dispose()
    {
     // 此处空出,因为我们不需要做什么操作。
    }
  
    private void AuthenticateRequest(object r_objSender,EventArgs r_objEventArgs)
    {
     // 鉴别用户的凭证,并找出用户角色。。
     1. HttpApplication objApp = (HttpApplication) r_objSender ;
     2. HttpContext objContext = (HttpContext) objApp.Context ;
     3. if ( (objApp.Request["userid"] == null) ||
     4.  (objApp.Request["password"] == null) )
     5.  {
     6.   objContext.Response.Write("<H1>Credentials not provided</H1>") ;
     7.   objContext.Response.End() ;
     8.  }
  
     9. string userid = "" ;
     10. userid = objApp.Request["userid"].ToString() ;
     11. string password = "" ;
     12. password = objApp.Request["password"].ToString() ;
  
     13. string[] strRoles ;
     14. strRoles = AuthenticateAndGetRoles(userid, password) ;
     15. if ((strRoles == null) || (strRoles.GetLength(0) == 0))
     16. {
     17.  objContext.Response.Write("<H1>We are sorry but we could not
  find this user id and password in our database</H1>") ;
     18.  objApp.CompleteRequest() ;
     19. }
  
     20. GenericIdentity objIdentity = new GenericIdentity(userid,
  "CustomAuthentication") ;
     21. objContext.User = new GenericPrincipal(objIdentity, strRoles) ;
    }
  
    private string[] AuthenticateAndGetRoles(string r_strUserID,string r_strPassword)
    {
     string[] strRoles = null ;
     if ((r_strUserID.Equals("Steve")) && (r_strPassword.Equals("15seconds")))
     {
      strRoles = new String[1] ;
      strRoles[0] = "Administrator" ;
     }
     else if ((r_strUserID.Equals("Mansoor")) && (r_strPassword.Equals("mas")))
     {
      strRoles = new string[1] ;
      strRoles[0] = "User" ;
     }
     return strRoles ;
    }
   }
  }
  
    我们研究一下上面的代码。
  
    我们是从Init函数开始的。这个函数把处理程序的AuthenticateRequest事件插入Application(应用程序)对象的事件处理程序列表中。这将导致引发AuthenticationRequest事件的时候Application调用该方法。
  
    我们的HTTP模块初始化之后,我们就可以调用它的AuthenticateRequest方法来鉴别客户端请求。AuthenticateRequest方法是该安全/身份认证机制的核心。在这个函数中:
  
    1和2行提取HttpApplication和HttpContext对象。3到7行检测是否没有给我们提供了用户id或密码。如果没有提供,就显示错误信息,请求处理过程终止。
  
    9到12行从HttpRequest对象中提取用户id和密码。
  
    14行调用一个叫做AuthenticateAndGetRoles的辅助(helper)函数。这个函数主要执行身份验证并决定用户角色。上面的代码采用了硬编码(hard-coded),只允许两个用户使用,但是我们可以扩展这个方法,并添加代码与用户数据库交互操作并检索用户的角色。
  
    16到19行检测是否有角色与用户关联。如果没有就意味着传递给我们的凭证没有通过验证;因此该凭证是无效的。因此,给客户端发送一个错误信息,并且请求结束了。
  
    20和21行非常重要,因为这两行实际上告诉ASP.NET HTTP运行时已登录用户的身份。这两行成功执行以后,我们的aspx页面就能够使用User对象访问这些信息了。
  
    现在我们看一看这种身份验证机制的运行情况。目前我们只允许下面两个用户登录到系统:
  
    · User id = Steve, Password = 15seconds, Role = Administrator
    · User id = Mansoor, Password = mas, Role = User
  
    注意用户id和密码是大小写敏感的(区分大小写)。
  
    首先试图不提供凭证登录系统,在IE中输入http://localhost/webapp2/index.aspx将看到下面的消息:
  
    
   
   现在试图使用用户id“Steve”和密码“15seconds”登录系统。输入 http://localhost/webapp2/index.aspx?userid=Steve&password=15seconds你将看到下面的欢迎消息:
  
   现在试图使用错误的用户id和密码组合来登录系统。输入http://localhost/webapp2/index.aspx?userid=Mansoor&password=xyz你将看到下面的错误消息:
  
  
  这表明我们的安全模块在起作用了。你可以通过在AuthenticateAndGetRoles方法中使用数据库访问代码来扩展该安全模块。
  
    要使所有的部分都起作用,我们必须对web.config文件进行一些修改。首先,由于我们要使用自己的身份验证,因此不需要其它的身份验证机制。为了达到这个目的,改变webapp2的web.config文件中的<authentication>节点,如下所示:
  
  <authentication mode="None"/>
  
    类似地,不允许匿名用户访问我们的Web站点。给web.config文件添加下面的语句:
  
  <authorization>
   <deny users="?"/>
  </authorization>
  
    用于至少能够匿名访问用于提供凭证的文件。在web.config文件中使用下面的配置设置信息把index.aspx作为唯一能够匿名访问的文件:
  
  <location path="index.aspx">
   <system.web>
    <authorization>
     <allow users="*"/>
    </authorization>
   </system.web>
  </location>
  
    结论
  
    你可能已经意识到有了HTTP处理程序和模块后,ASP.NET已经给开发者提供了强大的能量。把你自己的组件插入ASP.NET请求处理管道,享受它的优点吧。
  
    作为练习,你应该进一步改进程序,使示例身份验证模块更加灵活,并能根据用户的需要进行调整。

你可能感兴趣的:(asp.net)