评论对象: 云中漫步 | 2007/4/1 12:25:24
评论言论: 实现网址重写
网址重写技术不但可以在IIS Web服务器一级通过ISAPI过滤器实现,而且还可以在ASP.NET一级通过HttpModule或者 HttpHandler实现。本文主要关注在ASP.NET一级实现网址重写技术,所以此时不必关注在ISAPI应用程序中实现网址重写的技术细节,而且有很多第三方厂商提供的ISAPI过滤器。
构建网址重写引擎
在ASP.NET中实现网址重写很简单,只需调用System.Web.HttpContext类的 RewritePath()方法即可。 HttpContext类中包含有关于特定HTTP请求的HTTP规范信息。ASP.NET引擎每接收到一个特定请求后便针对该请求创建一个特定的实例,这个类包含一些属性诸如: Request和Response属性,分别提供对请求和响应的访问; Application和Session属性提供对Application变量和 Session变量的访问;User属性提供对已授权用户信息的访问。
在微软.NET Framework 1.0版本中,RewritePath()方法接收一个新路径的简单字符串,在其内部 HttpContext类的RewritePath(string)方法内在地更新Request对象的路径和查询参数。除了 RewritePath(string)方法之外,.NET Framework 1.1版还提供了另外一些重载版本,其中一个重载版本接收三个输入字符串参数,这种交替的重载形式不仅仅只是设置Request对象的路径和查询参数这些属性,而是设置更深层的成员变量,这些成员变量用于为 PhysicalPath、PathInfo、FilePath属性计算 Request对象值。
为了实现ASP.NET中的网址重写,我们需要创建一个HttpHandler和HttpModule用于:
·根据请求的路径决定所需要重写的路径;
·重写路径,如果需要的话可以调用RewritePath方法;
以前文所构建的那个站点为例,可以通过/info/employee.aspx?empID=EmployeeID来访问每一个雇员的信息。为了使这个网址更加地具有“隐蔽性”,我们可能会使用更加容易理解的访问方式如:/people/雇员名.aspx。这里就有了一个网址重写的案例:当接收到对/people/ScottMitchell.aspx的请求的时候,我们就得使用网址重写使得对该页面的请求被重写指向到先前使用的/info/employee?EmpID=1001地址。
使用HttpModule来调用网址重写
在ASP.NET一级来执行网址重写,既可以使用HttpHandler,也可以使用HttpModule。当使用 HttpModule的时候,必须决定如果该网址需要被重写的话,究竟应该在整个请求的生命周期期间的那一个点来使用。乍一看着有些武断,但是这个决定以重大而且微妙的方式影响到你的应用程序。之所以作出对网址重写点的选择是因为内嵌的ASP.NET HttpModule使用Request对象的属性值来完成自己的工作(回忆一下重写路径对 Request对象的属性值的改变),这些内嵌HttpModule和相应事件的密切关系列举如下:
HttpModule | 事件 | 简介 |
FormsAuthenticationModule | AuthenticateRequest | 判断用户是否已通过表单授权方式获取授权,如果没有的话则将用户重定向到指定的登录页面 |
FileAuthorizationModule | AuthorizeRequest | 当使用Windows授权方式的时候,该HttpModule判断并确定该Microsoft Windows帐户是否对其请求的资源拥有足够的权限 |
UrlAuthorizationModule | AuthorizeRequest | 检查并确认请求者是否对所访问的网址拥有权限。该Url授权可以在web.config文件的<authorization>和<location>元素中配置 |
回想一下BeginRequest事件在AuthenticateRequest事件之前引发,而 AuthenticateRequest事件又在AuthorizeRequest事件之前引发。
实现网址重写的一个较为安全的场合就是把它放在在BeginRequest事件中执行,这意味着如果要执行网址重写的话,在众多内嵌 HttpModule运行的时候他已经完成了。这种途径的最终用途淋漓尽致地体现在表单验证上。当用户访问受限资源的时候,如果之前使用了表单验证,他会自动被重定向到指定的登录页面,在成功登录之后,用户被重定向回先前试图访问的受限制页面。
如果把网址重写放在BeginRequest事件或者AuthenticateRequest事件中,在登录页面上执行提交后,该页面会将用户重定向到网址重写指定的页面。假定当用户在浏览器上敲入/people/ScottMitchell.aspx地址,该地址是要被重定向到/info/employee.aspx?EmpID=1001的,如果该Web应用程序设定使用表单验证,当用户开始访问/people/ScottMitchell.aspx的时候,该网址将重写指向/info/employee.aspx?EmpID=1001,接着 ForumAuthenticationModule启动,如果需要的话将用户重定向到登录页面,用户登录后重定向到的页面将是/info/employee.aspx?EmpID=1001,这也是自从 FormAuthenticationModule启动运行时所发出请求的页面。
同上类似,当把网址重写放在BeginRequest事件或者AuthenticateRequest事件中运行的时候,UrlAuthenticationModule也发现了网址重写指向的网址,这意味着如果在该应用程序的web.config文件中<location>节为特定的网址配置特定的授权地址的话,你得引用重写所指向的网址。
为了解决这个微妙的问题,一个可能就是把网址重写放在AuthorizeRequest事件中运行,但是在使用这种方法解决URL授权和表单授权的异常时又引入了一个新的缺陷:文件授权会失效。当使用Windows验证的时候, FileAuthorizationModule检查并验证已通过验证的用户是否拥有足够的权限访问特定的ASP.NET页面。
假定有一群用户并没有Windows级别的访问权限访问C:\inetpub\wwwroot\info\employee.aspx,当这些用户试图访问/info/employee.aspx?EmpID=1001的时候,他们会得到未授权的错误,如果我们把网址重写放到 AuthenticateRequest事件中运行,当FileAuthorizationModule验证该安全性设置的时候,他仍任人为被请求的文件是/people/ScottMitchell.aspx,而这时该网址已经被重写了,因此 FileAuthorizationModule会直接放行,让用户看到了网址重写指向的内容:/info/employee.aspx?Empid=1001。
那么什么时候在HttpModule调用网址重写合适呢?他决定于所使用的验证方式,当然如果不使用验证方式的话,那么无论是在 BeginRequest事件、AuthenticateRequest事件还是 AuthorizeRequest事件中调用网址重写没有多大区别,如果使用表单验证方式并且不使用Windows验证方式的话,把网址重写放入 AuthorizeRequest事件委托中调用既可,如果使用Windows验证方式的话,把这项功能放入 BeginRequest事件或者AuthenticateRequest事件调用就行了。
使用HttpHandler来调用网址重写
除了上面所述方法外,网址重写也可以放入HttpHandler或者HttpHandlerFactory中调用。 HttpHandler是一个负责针对特定请求生成相应内容的类,而HttpHandlerFactory返回一个HTTP的实例,该实例针对特定请求生成相应内容。
本节将着眼于为这些ASP.NET页面创建一个网址重写的HttpHandlerFactory。创建 HttpHandlerFactory必须实现IHTTPHandlerFactory接口,它包括一个 GetHandler()方法。ASP.NET引擎在初始化这些HttpModule后做出决定针对该请求调用相应的 HttpHandler或者HttpHandlerFactory,在调用 HttpHandlerFactory的时候,针对该Web请求以及随同的其他信息的HttpContext中经过的的 HttpHandlerFactory的GetHandler()方法将被ASP.NET引擎调用, HttpHandlerFactory必须返回一个能委托该请求的对象,并且该对象要能实现 IHttpHandler接口。
要通过一个HttpHandler来调用网址重写,可以先创建一个HttpHandlerFactory,它的 GetHandler()方法检查所请求的网址并决定是否需要调用网址重写。如果要调用网址重写的话则调用前文所述的已通过检查的 HttpContext对象的RewritePath()方法。最后该 HttpHandlerFactory返回一个由类 System.Web.UI.PageParser的GetCompiledInstance()方法返回的 HttpHandler。(这与内嵌于ASP.NET页面的HttpHandlerFactory( PageHandlerFactory)的工作原理相同。)
在所有HttpModule被初始化后,HttpHandlerFactory就开始被实例化。把网址重写放在这些事件场所的最后一个里头调用的时候,也会碰到相同的问题:文件授权将会失效。如果非要依赖于Windows验证和文件验证的时候,你可能得使用 HttpModule来调用网址重写了。
下一章我们着眼于如何构建一个可重用的网址重写引擎,使用下文所提的这些示例均以真实案例作为参照,在作者主页上提供下载。先用用一个简单的网址重写的例子来探讨如何实现网址重写,紧接着将利用网址重写引擎中正则表达式的强大处理能力来展示真正“隐蔽”的网址重写技术!
使用网址重写引擎实现简单的网址重写
为了便于在Web应用程序中实现网址重写,我构建了一个网址重写引擎,该引擎提供下列功能:
·可以在web.config文件中为页面开发者定义其所使用的网址重写引擎的规则;
·通过使用正则表达式来使所制定的网址重写规则具有更加强大的重写能力;
·能够通过简单配置即可在HttpModule和HttpHandler中使用网址重写。
本节只探讨通过HttpModule来实现网址重写,要了解如何通过HttpHandler来实现网址重写请下载本文提供的代码。
1. 设置网址重写引擎的配置信息
我们来探讨一下在web.config中网址重写规则的配置节。首先必须在web.config文件中指出是否需要在HttpHandler或者 HttpModule中调用网址重写,在web.config中,下文已经包含了两个已经被注释掉的配置节:
<!--
<HttpModules>
<add type="URLRewriter.ModuleRewriter,URLRewriter" name="ModuleRewriter"/>
</HttpModules>
-->
<!--
<httpHandlers>
<add verb="*" path="*.aspx" type="URLRewriter.RewriterFactoryHandler,URLRewriter" />
</httpHandlers>
-->
被注释掉的<HttpModules>为配置使用HttpModule调用网址重写;注释掉的<httpHandler>为配置使用 HttpHandler调用网址重写。
不论配置使用<HttpModules>还是<httpHandlers>调用网址重写,除此之外还须配置网址重写规则,一条重写规则包括两项字符串:请求URL中的查找模式和针对该模式的匹配成功后的替换字符串。该信息在web.config文件中用下列标签描述:
<RewriterConfig>
<Rules>
<RewriterRule>
<LookFor>pattern to look for</LookFor>
<SendTo>String to replace pattern with </SendTo>
</RewriterRule>
<RewriterRule>
<LookFor>pattern to look for</LookFor>
<SendTo>String to replace pattern with </SendTo>
</RewriterRule>
</Rules>
</RewriterConfig>
每一条规则都用一个<RewriterRule>元素表示,以<LookFor>节表示查询模式,当查询模式发现匹配字符串时便用<SendTo>节表示的字符串进行替换。这些规则从上到下进行查询匹配,如果找到一个匹配则按此规则执行网址重写,并且停止查找。
配置<LookFor>节要使用正则表达式来进行字符串匹配和替换。(在此我们举一个例子来说明如何使用正则表达式来对字符串进行匹配和替换。)既然该查找模式是一个正则表达式,那么要注意避开对正则表达式保留字符串的直接使用。(正则表达式的保留字符串包括有:.,?,^,$,等等,可以通过在前面加上一个反斜线来引用这些保留字符,例如\.表示引用一个句点)
2. 使用HttpModule来执行网址重写
创建一个HttpModule很简单,只要创建一个实现IHttpModule接口的类,该 IHttpModule接口定义了两个方法:
·Init(HttpApplication),该方法在HttpModule初始化时引发,通过该方法为 HttpApplication事件调用相应的事件委托;
·Dispose(),当相应请求处理结束并发送回IIS调用此方法,通过此方法执行最终所有的清理和回收程序。
为了更加方便地为网址重写创建HttpModule,从一开始我就创建一个抽象的基类( BaseModuleRewriter),该类实现了IHttpModule接口。在 Init(HttpApplication)事件中,它通过BaseModuleRewriter_AuthorizeRequest方法引发了 HttpApplication的AuthorizeRequest事件,该 BaseModuleRewriter_AuthorizeRequest方法通过该类的Rewrite()方法重写传入参数 HttpApplication对象的内部请求虚拟路径(Path)。在 BaseModuleRewriter对象中,该Rewrite()方法是抽象的,并且没有实际内容,但在继承自该类的对象中必须重载 Rewrite()方法并为该方法提供实际内容。
通过对该基类的继承,所有需要做的工作就是创建一个继承自BaseModuleRewriter的类,重载 Rewrite()方法并在该方法中添加网址重写逻辑代码。下文列出BaseModuleRewriter代码:
public abstract class BaseModuleRewriter : IHttpModule
onclick="this.style.display=''none''; Codehighlighter1_55_680_Open_Text.style.display=''none''; Codehighlighter1_55_680_Closed_Image.style.display=''inline''; Codehighlighter1_55_680_Closed_Text.style.display=''inline'';" alt="" src="http://soft.yesky.com/imagelist/06/31/7158q573bnkh.gif" align=top>onclick="this.style.display=''none''; Codehighlighter1_55_680_Closed_Text.style.display=''none''; Codehighlighter1_55_680_Open_Image.style.display=''inline''; Codehighlighter1_55_680_Open_Text.style.display=''inline'';" alt="" src="http://soft.yesky.com/imagelist/06/31/f02701a0ji2d.gif" align=top> {
onclick="this.style.display=''none''; Codehighlighter1_106_364_Open_Text.style.display=''none''; Codehighlighter1_106_364_Closed_Image.style.display=''inline''; Codehighlighter1_106_364_Closed_Text.style.display=''inline'';" alt="" src="http://soft.yesky.com/imagelist/06/31/g818508mguup.gif" align=top>onclick="this.style.display=''none''; Codehighlighter1_106_364_Closed_Text.style.display=''none''; Codehighlighter1_106_364_Open_Image.style.display=''inline''; Codehighlighter1_106_364_Open_Text.style.display=''inline'';" alt="" src="http://soft.yesky.com/imagelist/06/31/6e8y5mh50697.gif" align=top> public virtual void Init(HttpApplication app) {
// WARNING! This does not work with Windows authentication!
// If you are using Windows authentication,
// change to app.BeginRequest
app.AuthorizeRequest += new EventHandler(this.BaseModuleRewriter_AuthorizeRequest);
}
onclick="this.style.display=''none''; Codehighlighter1_401_402_Open_Text.style.display=''none''; Codehighlighter1_401_402_Closed_Image.style.display=''inline''; Codehighlighter1_401_402_Closed_Text.style.display=''inline'';" alt="" src="http://soft.yesky.com/imagelist/06/31/70wb98m5143q.gif" align=top>onclick="this.style.display=''none''; Codehighlighter1_401_402_Closed_Text.style.display=''none''; Codehighlighter1_401_402_Open_Image.style.display=''inline''; Codehighlighter1_401_402_Open_Text.style.display=''inline'';" alt="" src="http://soft.yesky.com/imagelist/06/31/240t55145s6h.gif" align=top> public virtual void Dispose() {}
onclick="this.style.display=''none''; Codehighlighter1_495_597_Open_Text.style.display=''none''; Codehighlighter1_495_597_Closed_Image.style.display=''inline''; Codehighlighter1_495_597_Closed_Text.style.display=''inline'';" alt="" src="http://soft.yesky.com/imagelist/06/31/5251hl8696y8.gif" align=top>onclick="this.style.display=''none''; Codehighlighter1_495_597_Closed_Text.style.display=''none''; Codehighlighter1_495_597_Open_Image.style.display=''inline''; Codehighlighter1_495_597_Open_Text.style.display=''inline'';" alt="" src="http://soft.yesky.com/imagelist/06/31/94cr6m02a593.gif" align=top> protected virtual void BaseModuleRewriter_AuthorizeRequest(object sender, EventArgs e) {
HttpApplication app = (HttpApplication) sender;
Rewrite(app.Request.Path, app);
}
protected abstract void Rewrite(string requestedPath, HttpApplication app);
}
注意:该BaseModuleRewriter类将网址重写放在AuthorizeRequest事件中调用,如果要使用Windows验证并使用文件验证模式时请修改代码将网址授权放在 BeginRequest或者AuthenticateRequest事件中。
ModuleRewriter继承自BaseModuleRewriter,并真正意义地实现了网址重写的操作,该类仅包含一个重载了的方法 Rewrite(),其内容如下文所示:
protected override void Rewrite(string requestedPath, System.Web.HttpApplication app)
onclick="this.style.display=''none''; Codehighlighter1_86_1078_Open_Text.style.display=''none''; Codehighlighter1_86_1078_Closed_Image.style.display=''inline''; Codehighlighter1_86_1078_Closed_Text.style.display=''inline'';" alt="" src="http://soft.yesky.com/imagelist/06/31/492ct2qhauh7.gif" align=top>onclick="this.style.display=''none''; Codehighlighter1_86_1078_Closed_Text.style.display=''none''; Codehighlighter1_86_1078_Open_Image.style.display=''inline''; Codehighlighter1_86_1078_Open_Text.style.display=''inline'';" alt="" src="http://soft.yesky.com/imagelist/06/31/742z4vx92wht.gif" align=top> {
// get the configuration rules
RewriterRuleCollection rules = RewriterConfiguration.GetConfig().Rules;
// iterate through each rule
for(int i = 0; i < rules.Count; i++)
onclick="this.style.display=''none''; Codehighlighter1_280_1076_Open_Text.style.display=''none''; Codehighlighter1_280_1076_Closed_Image.style.display=''inline''; Codehighlighter1_280_1076_Closed_Text.style.display=''inline'';" alt="" src="http://soft.yesky.com/imagelist/06/31/e31g487j3j1r.gif" align=top>onclick="this.style.display=''none''; Codehighlighter1_280_1076_Closed_Text.style.display=''none''; Codehighlighter1_280_1076_Open_Image.style.display=''inline''; Codehighlighter1_280_1076_Open_Text.style.display=''inline'';" alt="" src="http://soft.yesky.com/imagelist/06/31/682653l233o0.gif" align=top> {
// get the pattern to look for, and
// Resolve the Url (convert ~ into the appropriate directory)
string lookFor = "^" +
RewriterUtils.ResolveUrl(app.Context.Request.ApplicationPath, rules[i].LookFor) + "$";
// Create a regex (note that IgnoreCase is set )
Regex re = new Regex(lookFor, RegexOptions.IgnoreCase);
// See if a match is found
if (re.IsMatch(requestedPath))
onclick="this.style.display=''none''; Codehighlighter1_729_1070_Open_Text.style.display=''none''; Codehighlighter1_729_1070_Closed_Image.style.display=''inline''; Codehighlighter1_729_1070_Closed_Text.style.display=''inline'';" alt="" src="http://soft.yesky.com/imagelist/06/31/485hfil868ai.gif" align=top>onclick="this.style.display=''none''; Codehighlighter1_729_1070_Closed_Text.style.display=''none''; Codehighlighter1_729_1070_Open_Image.style.display=''inline''; Codehighlighter1_729_1070_Open_Text.style.display=''inline'';" alt="" src="http://soft.yesky.com/imagelist/06/31/e4r0je6c95wz.gif" align=top> {
// match found - do any replacement needed
string sendToUrl = RewriterUtils.ResolveUrl(app.Context.Request.ApplicationPath, re.Replace(requestedPath, rules[i].SendTo));
// Rewrite the URL
RewriterUtils.RewriteUrl(app.Context, sendToUrl);
break; // exit the for loop
}
}
}
该Rewriter()方法以获取web.config文件中的网址重写规则的设置为起始,它通过循环访问各条网址重写规则,每次均获取当前规则中的 LookFor属性,用正则表达式验证并判断是否查找是否对当前请求的网址是否有匹配。
如果发现一条匹配,将用当前规则的SendTo值对请求的路径执行一个正则表达式替换,替换后的地址通过参数的形式传给RewriterUtils.RewriteUrl()方法, RewriterUtils是一个帮助类,它提供一对HttpModule和 HttpHandler都可以使用的静态方法,RewriterUrl()方法只是简单地调用了 HttpContext对象的RewritePath()方法。
注意:你已经注意到了当执行正则表达式匹配和替换的时候调用了一个RewriterUtils.ResolveUrl()方法。该帮助方法简单地替换了应用程序路径中“~”的所有实例。
我们已经探讨了主要的部分,但是还有其它一些组件诸如将web.config文件中XML格式化了的网址重写规则反序列化至一个对象的类定义、通过 HttpHandlerFactory实现网址重写的类定义等。本文最后三节将通过一些真实案例来探讨网址重写的技术。