之 前觉得这个话题已经被谈滥了。URL Rewrite早已经被广大开发人员所接受,网上关于URL Rewrite的组件和文章也层出不穷,但是总是让我感觉意犹未尽,于是最终还是忍不住提笔写了这系列文章。这些文章不会谈论URL Rewrite的价值与意义,而只会谈论纯技术的内容。文章中也不会有详尽地实现分析,而是结合了我的经验,从应用角度来讲解这个话题。您已经知道的,您 还不知道的,别处已经讲过的,或者还没有讲过的,希望这系列文章的“旧事重提”不会让您觉得沉闷,并且能让您了解ASP.NET中URL
Rewrite的方方面面。如果您以后再遇到URL Rewrite方面的问题是能够想到这几篇文章,估计我做梦也会笑出声来。
要充分理解文章后面谈到的话题,我们必须简单的了解一下IIS与ASP.NET的通信过程。我在这里讲解的是IIS 6服务器。至于IIS 5和IIS 7,前者可以说已经被淘汰了,而后者的“经典模式”与IIS 6可谓如出一辙,而新的“管道模式”其实是讲ASP.NET中的某些概念与IIS进行了深度集成。我相信,如果您了解了IIS 6和ASP.NET,在IIS 7的集成模式下也不会有任何问题。
首先我们来看一幅简单的示意图,展示了IIS从收到Request开始,到返回Response整个过程中的几个主要步骤:
- IIS收到请求。
- 选择器根据URL的特点与IIS中的配置,选择一个ISAPI用于处理该请求——现在自然会选择ASP.NET ISAPI。
- ASP.NET执行引擎接收到请求,于是初始化数据(例如构建各种对象)。
- 开始触发各种Pipeline事件,自然先从BeginRequest开始。
- 经过了多个Pipeline事件,ASP.NET根据配置为当前请求选择一个合适的Handler或HandlerFactory进行处理(当然特殊情况例外,例如已经在之前的事件中直接输出结果并结束请求了)。
- 经过了Handler处理之后又经过几个Pipeline事件,以EndRequest结束。
- 输出Response。
在一个ASP.NET应用中如果要进行URL Rewrite,那么一般就是在BeginRequest事件中调用HttpContext的RewritePath方法,将该请求重新“定位”至一个目 标URL。例如我们就可以在Global.asax中重写Application_BeginRequest方法来实现这一点:
之所以在BeginRequest中进行Rewrite,是因为这个事件是在所有Pipeline事件中最早被触发的。在这时进行了重新“定位”之 后,当前HttpContext中的一些属性也就发生了相应的变化(例如HttpContext.Request.Path)。这样,接下来的 Pipeline事件的处理程序逻辑就会受到影响。例如在需要根据目录进行权限判断时,就会使用“定位”后的路径,而不是ASP.NET所收到的请求。自 然最“显著”的变化就是对Handler的选择,例如上例,我们把请求重新定位至“CustomerList.aspx”文件,这样ASP.NET引擎就
会选择*.aspx所对应的System.Web.UI.PageHandlerFactory类对请求进行处理。
public class Global : System.Web.HttpApplication { protected void Application_BeginRequest(object sender, EventArgs e) { HttpContext context = HttpContext.Current; if (context.Request.Path.Equals("/Customers", StringComparison.InvariantCultureIgnoreCase)) { context.RewritePath("~/CustomerList.aspx"); } } }
最后插句提外话,有两个概念需要区分开来,那就是“ASP.NET Pipeline”与“Web Forms”。两者都是ASP.NET里的重要模型,但是差别还是非常大的:
- ASP.NET Pipeline:作为每个ASP.NET应用所接受到的请求来说,都会经过这个“管道”进行处理。这是一个ASP.NET级别的模型。
- Web Forms:在ASP.NET Pipeline的执行过程中,其中有一个步骤是选择一个合适的Handler(或HandlerFactory)来处理请求。如果是aspx页 面,ASP.NET就会选择System.Web.UI.PageHandlerFactory类,在这个类中才最终形成了WebForms模型。
其实上面这句话的“形成”二字可能也不太确切。因为Web Forms可能应该是一个可以独立使用的执行引擎和模型,而System.Web.UI.PageHandlerFactory中也只是利用了这个模型而 已。我们在编写ASP.NET应用时,完全可以根据我们的需要,在其他地方使用这个模型。例如在《技巧:使用User
Control做HTML生成》一文中,我们就在一个Generic Handler中把ascx当作模板来生成内容。
相关链接:
重提URL Rewrite(2):使用已有组件进行URL Rewrite
2008-01-13 00:27 by Jeffrey Zhao, 18998 visits,
网摘,
收藏,
编辑
可能已经没有人会使用上一篇文章中的方法进行URL Rewrite了,因为提供URL Rewrite的组件早已铺天盖地了。
ASP.NET级别的URL Rewrite组件的原理很简单,其实只是监听BeginRequest事件,并且根据配置来决定目标URL。在我之前接触过的项目中,发现使用URLRewriter作为URL Rewrite组件的频率非常高,我想可能是因为那是微软提供的东西吧。
如果要使用URLRewriter,首先自然就是在web.config中配置一个HttpModule:
<httpModules> <add name="ModuleRewriter" type="URLRewriter.ModuleRewriter, URLRewriter" /> </httpModules>
然后就是进行配置了(注:强烈建议使用configPath属性将配置提取成额外的文件,便于管理):
<configSections> <section name="RewriterConfig" type="URLRewriter.Config.RewriterConfigSerializerSectionHandler, URLRewriter" /> </configSections> <RewriterConfig> <Rules> <RewriterRule> <LookFor>~/tag/([\w]+)/</LookFor> <SendTo>~/Tags.aspx?Tag=$1</SendTo> </RewriterRule> </Rules> </RewriterConfig>
正 则表达式是一个非常了不得的东西,能匹配,能捕获。在上面的例子中,我们把符合LookFor条件的“/tag/xxx”重新定位到Tags.aspx页 面上,并且将xxx作为Tag这个QueryString项的值,这样就能够在代码中通过 HttpContext.Request.QueryString["Tag"]来获得该值了。
URLRewriter的 功能对于大多数应用来说已经足够了,但是我总是不喜欢。但如果非要问我不喜欢的原因,我也难说出个子丑寅卯来。可能仅仅是这个配置方式的问题吧。在使用 URL Rewriter时,配置段往往会非常长,每个配置项需要从<RewriterRule>到</RewriterRule>共4
行代码,一个规模不大的项目都很容易出现上百行的配置。“这也太XML了”,我想,为什么不用XML Attribute呢?这样每个配置项就能缩短为1行了——不过,这是题外话。
所以如果我目前要做URL Rewrite,往往用的是Intelligencia出品的开源组件UrlRewriter.NET。虽然这个名字和前一个非常相似,但是功能却远超前者。该组件在使用上和URLRewriter比较接近(其实似乎所有的URL Rewrite组件都差不多),我们要做的也只是配置:
<configSections> <section name="rewriter" type="Intelligencia.UrlRewriter.Configuration.RewriterConfigurationSectionHandler, Intelligencia.UrlRewriter" /> </configSections> <rewriter> <rewrite url="^/User/(\d+)$" to="~/User.aspx?id=$1" processing="stop" /> <rewrite url="^/User/(\w+)$" to="~/User.aspx?name=$1" processing="stop" /> </rewriter> <system.web> <httpModules> <add name="UrlRewriter" type="Intelligencia.UrlRewriter.RewriterHttpModule, Intelligencia.UrlRewriter" /> </httpModules> </system.web>
我 们主要来看一下重写规则的配置项<rewriter />。与URLRewriter不同的是,UrlRewriter.NET使用了我喜欢的每规则一个节点的方式,这让整个项目的重写规则简洁不少。 不过processing="stop"又是什么意思?这就要谈到UrlRewriter.NET在处理重写规则时的方法了。 UrlRewriter.NET在找到一个匹配的重写规则时,不会就此停止,而会继续寻找其余的匹配项,最终生效的则是能够匹配当前请求的最后一个重写规 则。如果我们需要UrlRewriter.NET在找到某个匹配项之后即生效,就需要将processing属性设为stop。例如在上面的配置里,如果
“/User/”后紧跟着数字,则会使用用户ID进行查找,否则则认为当前所提供的是用户名。
如果UrlRewriter.NET仅仅是因为配置上显得比较简洁,它与URLRewriter相比实在没有什么优势。但是UrlRewriter.NET的能力远不止此,我们刚才使用的其实只是它提供的Act
- if
- unless
- rewrite
- redirect
- setstatus
- forbidden
- gone
- not-allowed
- not-found
- not-implemented
- addheader
- setcookie
- setproperty
光 有Act
“得 组件如此,夫复何求”,不过我在这里还是要推荐另外一个组件。因为在某些特殊情况下,UrlRewriter.NET还不能满足我们的要求。嗯?不是能自 行扩展吗?没错,可是——先卖个关子,本系列的最后一篇中来说明这个问题。UrlRewriter.NET提供了ASP.NET层面上的URL Rewriter。如果要在IIS层面上进行URL Rewrite,那么还必须使用其他方式。ISAPI
Rewrite是IIS层面上进行URL Rewrite的著名组件,很可惜这是个商业组件,需要我们使用美刀来购买。因此我在这里推荐另一个开源产品:IIRF(Ionic's Isapi Rewrite Filter)。
由于是在IIS层面进行URL Rewrite,IIRF的配置方式和UrlRewriter.NET是不同的。如果要使用IIRF,则需要将IsapiRewrite4.dll添加到Web Site的ISAPI Filter列表中:
IIRF是通过ini文件来配置的,IsapiRewrite4.ini与IsapiRewrite4.dll放在同样的目录中即可:
RewriteRule ^/User/(\d+)$ /User.aspx?id=$1 [I, L] RewriteRule ^/User/(\w+)$ /User.aspx?name=$1 [I, L]
IIRF 的重写规则是“RewriteRule <url-pattern> <destination> [<modifiers>]”,每个部分之间的空格数目没有限制,不过一定要是空格,而不能是Tab等其他空白字符。“url- pattern”和“destination”自不必多说,关键在于modifier。IIRF的modifier有不少,在这里我先只介绍上面用到的两 个。“I”表示匹配时大小写无关,“L”的作用和UrlRewriter.NET里的processing="stop"类似,IIRF在找到该匹配项时
立即生效,而不会继续查找下去。
IIRF虽然是一个开源的组件,但是功能依然比较强大。尤其是结合了 RewriteCond(Rewrite Condition)之后,可以实现比较复杂的重写规则。例如以下的配置则把UserAgent里包含Googlebot字样的根目录请求重写到某个特定 的资源上:
RewriteCond %{HTTP_USER_AGENT} ^Googlebot.* RewriteRule ^/ $/Googlebot.html [L]
最 后,我们来看一下两种组件Rewrite的区别。显然,最大的区别就在于它们是不同层面上的重写,下面的两幅示意图就描述了在两种情况下它们是如何将原本 应该得到“404 Resource Not Found”结果的“/User/jeffz”请求重写至“/User/name=jeffz”的。
首先是UrlRewriter.NET在ASP.NET层面上的URL Rewrite:
接着是IIRF在IIS层面上的URL Rewrite:
有了这两个组件,相信我们已经再也不需要其他东西来实现URL Rewrite了。
重提URL Rewrite(3):在URL Rewrite后保持PostBack地址
2008-01-13 03:17 by Jeffrey Zhao, 14892 visits,
网摘,
收藏,
编辑
在进行了URL Rewrite之后,经常会遇到的问题就是页面中PostBack的目标地址并非客户端请求的地址,而是URL Rewrite之后的地址。以上一篇文章中的重写为例:
<rewriter> <rewrite url="^/User/(\d+)$" to="~/User.aspx?id=$1" processing="stop" /> <rewrite url="^/User/(\w+)$" to="~/User.aspx?name=$1" processing="stop" /> </rewriter>
当 用户请求“/User/jeffz”之后,页面中的出现的代码却会是<form act
<script language="javascript" type="text/javascript"> document.getElementsByTagName("form")[0].action = window.location; </script>
这行代码的意图非常明显,将form的act
因为太丑了。
因 为我们还是把URL Rewrite之后的地址暴露给了客户端。用户只要装一个HTTP嗅探器(例如著名的Fiddler),或者在IE中直接选择查看源文件,我们的目标地址 就毫无遮掩的显示在用户面前了。怎么能让用户知道我们的重写规则?我们必须解决这个问题。解决的方法很简单,也已经非常流行了,那就是使用Control Adaptor来改变Form生成时的行为。不过让我感到比较奇怪的是,关于这个Control Adaptor,在网络上搜到的尽是VB.NET的版本,倒是微软主推的C#语言却找不到。虽然只要了解一点VB.NET的语法要改写起来并不困难,但是
毕竟也是个额外的工作啊。所以我现在就将这个Adaptor的C#版本代码贴出来,以便朋友们能够直接使用:
namespace Sample.Web.UI.Adapters { public class FormRewriterControlAdapter : System.Web.UI.Adapters.ControlAdapter { protected override void Render(HtmlTextWriter writer) { base.Render(new RewriteFormHtmlTextWriter(writer)); } } public class RewriteFormHtmlTextWriter : HtmlTextWriter { public RewriteFormHtmlTextWriter(HtmlTextWriter writer) : base(writer) { this.InnerWriter = writer.InnerWriter; } public RewriteFormHtmlTextWriter(TextWriter writer) : base(writer) { this.InnerWriter = writer; } public override void WriteAttribute(string name, string value, bool fEncode) { if (name == "action" ) { HttpContext context = HttpContext.Current; if (context.Items["ActionAlreadyWritten"] == null) { value = context.Request.RawUrl; context.Items["ActionAlreadyWritten"] = true; } } base.WriteAttribute(name, value, fEncode); } } }
简 单的说,这个Control Adaptor其实一直在等待“act
不过要让这个Control Adaptor生效,还必须在Web项目中创建一个browser文件,例如“App_Browsers\Form.browser”,在里面写入如下代码:
<browsers> <browser refID="Default"> <controlAdapters> <adapter controlType="System.Web.UI.HtmlControls.HtmlForm" adapterType="Sample.Web.UI.Adapters.FormRewriterControlAdapter" /> </controlAdapters> </browser> </browsers>
至 此,在ASP.NET层面上作URL Rewrite导致PostBack地址改变的问题已经完美解决了——等等,为什么要强调“ASP.NET层面”?没错,因为如果在IIS层面上作URL Rewrite,这个问题依旧存在。例如您使用了IIRF做URL Rewrite,并让上面的Control Adapter生效,还是会发现页面上PostBack的地址和客户端请求的地址不同。难道RawUrl也变得“不忠诚”了?这不是RawUrl的缘故, 而是ASP.NET机制所决定的。为了解释这个问题,我们重新看一下在第一篇文章《IIS与ASP.NET》中那幅示意图:
IIS 级别的URL Rewrite发生在上面这幅图中步骤2之前,正因为被重新Rewrite了,所以IIS的ISAPI选择器才会将该请求交给ASPNET ISAPI处理。换句话说,当IIS把请求交由ASP.NET引擎处理的时候,ASP.NET从IIS那里获得的信息中已经是URL Rewrite之后的地址了(例如/User.aspx?name=jeffz),这样无论在ASP.NET处理该请求的哪个环节,都无法得知IIS当初 收到请求时的URL。
也就是说,其实真没办法了。
不过“真没办法”四个字是有条件的,完整地说应该是:“靠 ASP.NET自身”的确“真没办法”了。不过如果IIS在进行URL Rewrite的时候帮我们一把,那么情况又会如何呢?IIRF作为一个成熟的开源组件,它自然知道ASP.NET引擎,乃至所有的ISAPI处理程序都 需要它的帮助,它自然知道“改出手时就出手”的道理,因此它练就了将原始地址存放在服务器变量HTTP_X_REWRITE_URL之中的能力。不过 IIRF也不会“自觉”地这么做(多累啊),这还要我们在配置文件中提醒它:
RewriteRule ^/User/(\d+)$ /User.aspx?id=$1 [I, L, U] RewriteRule ^/User/(\w+)$ /User.aspx?name=$1 [I, L, U]
请 注意,我们使用了额外的Modifier。在Modifier集合中加入U表明我们需要IIRF将URL Rewrite之前的原始地址存放在服务器变量HTTP_X_REWRITE_URL中。现在我们就可以在ASP.NET获取到这个值了,于是我们将之前 的Control Adapter代码中的WriteAttribute方法作如下修改:
public override void WriteAttribute(string name, string value, bool fEncode) { if (name == "action" ) { HttpContext context = HttpContext.Current; if (context.Items["ActionAlreadyWritten"] == null) { value = context.Request.ServerVariables["HTTP_X_REWRITE_URL"] ?? context.Request.RawUrl; context.Items["ActionAlreadyWritten"] = true; } } base.WriteAttribute(name, value, fEncode); }
现在act
至此,有关URL Rewrite的主要话题已经讲完了,在下一篇,也就是本系列的最后一篇文章中,我们将重点看一下使用不同层面的URL Rewrite会在一些细节方面造成什么样的区别,以及相关的注意点。
相关链接:
(1)IIS与ASP.NET
重提URL Rewrite(4):不同级别URL Rewrite的一些细节与特点
2008-01-13 16:35 by Jeffrey Zhao, 15280 visits,
网摘,
收藏,
编辑
在之前的文章里我们已经谈论了有关URL Rewrite的几个主要的方面。在本系列的最后一篇文章中,我们就来讨论一下有关不同级别URL Rewrite的一些细节与特点。
理 论上说,IIS级别的URL Rewrite使用C或C++编写,比使用托管代码编写的ASP.NET级别URL Rewrite性能要高。但是我认为这方面的差距在大部分情况下可以忽略不计,这种性能几乎不可能成为性能瓶颈。因此选择何种级别的URL Rewrite一般不会由您应用程序的性能要求来决定。那么到底应该使用哪种级别的URL Rewrite呢?在使用不同级别的URL Rewrite之后,我们又该注意点什么呢?我在这里谈谈我个人的看法。
对URL Rewrite功能上的要求
虽说 目前的URL Rewrite组件在功能上已经能够满足大部分的应用,但是在某些时候,我们的确还是会需要一些特殊的功能。例如根据域名进行URL Rewrite,就目前的URL Rewrite组件来说,想要实现这个并不容易。商业化的ISAPI Rewrite目前已经可以支持这一点,可惜开源的UrlRewriter.NET和IIRF在这方面功能都有所不足。它们都是根据请求相对于该站点的路 径来匹配,至于请求的是哪个域名并不能作为匹配条件来使用。这就要求我们对URL Rewrite组件进行扩展。对于大部分.NET开发人员来说,托管代码自然是开发首选,这时可能就要选择ASP.NET级别的URL
Rewrite重写组件了。不过目前网上能找到不少扩展的例子,无论是ASP.NET级别的UrlRewriter.NET还是IIS级别的IIRF。
不过事实上,如果要实现上述功能,我们也可以分两步进行。首先我们在IIS级别使用IIRF进行URL Rewrite,接着在ASP.NET级别作进一步的URL Rewrite。例如我们现在要实现将“http://jeffz.domain.com/articles”重写为“/ArticleList.aspx?owner=jeffz”,就可以先在让IIRF做第一次URL Rewrite,目的是将“/articles”重写至“/ArticleList.aspx”。
RewriteRule ^/Articles$ /ArticleList.aspx [I, L, U]
这 样,ASP.NET引擎就会直接接收到一个针对/ArticleList.aspx的请求了。然后在ASP.NET内部,我们可以作第二次的URL Rewrite(方便起见,我这里还是在Global.asax里写,在项目中还是建议使用额外的HttpModule来实现)。
protected void Application_BeginRequest(object sender, EventArgs e) { HttpContext context = HttpContext.Current; string host = context.Request.Url.Host; string owner = host.Substring(0, host.IndexOf('.')); context.RewritePath(context.Request.RawUrl + "?owner=" + owner); }
经过两次URL Rewrite,已经实现了我们想要的效果(在实际项目中,上面的代码不能直接使用,因为需要判断是否有Query String等等)。
此 外,ASP.NET级别的URL Rewrite只能在ASP.NET里工作(显然的事情),如果要让URL Rewrite支持PHP,RoR等其他服务器技术,就只能使用IIS级别的URL Rewrite了(或者其他服务器技术提供的URL Rewrite功能)。
对URL中特殊字符的处理
有些特殊字符是不允许出现在URL中的,或者一旦出现在URL里以 后,请求的含义就被改变了。例如我们需要对搜索页面进行URL Rewrite,将“/Search/xxx”重写为“/Search.aspx?xxx”,然后可以根据问号后面的字符串获得用户提供的关键字。如果使 用UrlRewriter.NET,我们就会使用如下的配置:
<rewriter> <rewrite url="^/Search/(.+)$" to="~/Search.aspx?$1" processing="stop" /> </rewriter>
普通情况下,这个URL Rewrite工作正常。但是如果用户使用“%” 作为关键字,情况就不一样了,因为我们会收到如下的错误页面提示:
这 是因为URL中是不允许出现“%”的。大家可以去各种网站上尝试着请求一些例如“ABC%25DEF”的路径(“%25”之后即为“%”),大都能发现 “400 Bad Request”错误。不过将“%”放在Query String里倒是合法的——对阿,我们不是将keyword重写到Query String里了吗?为什么还是不行呢?这还是由于ASP.NET执行方式决定的。
Bad Request是在上图的步骤3,也就是还在进行初始化的时候就被确定了。而我们的URL Rewrite是在第4步BeginRequest事件中才发生的。当请求中带有非法字符时,我们根本还没有机会进行URL Rewrite。
那么我们怎么处理这个问题呢?在一般情况下,我们在客户端将%去除也不会有太大问题(有些站点的确是这么做的),但是如果非要保留呢?那么就使用Query String来传递参数吧,或者我们也可以使用IIS级别的URL Rewrite。还是以IIRF为例:
RewriteRule ^/Search/(.+)$ /Search.aspx?$1 [I, L, U]
当请求被发送到IIS之后(步骤一),并且在选择应该交给哪个ISAPI执行(步骤二)之前就发生了URL Rewrite。经过了URL Rewrite之后的地址,其中的“%”已经被转移到了Query String中,这时候交由ASP.NET处理时自然已经合法了。
出错页面配置
最后我们来讨论出错页面的配置。例如,一般来说我们都会为应用配置一个404错误页面,这样用户在访问一个不存在的资源时我们可以给他查看一个特定的页面,而不是默认的错误提示。但是在这一点上,不同级别的URL Rewrite就要使用不同的方法进行配置。
如 果我们使用了ASP.NET级别的URL Rewrite,一般来说我们已经在IIS里设置了Wildcard Mapping,这样任意的请求(包括html,jpg等)都会交由ASP.NET处理。如果请求了一个不存在的资源,404错误将由ASP.NET发 出,因此404错误页面应该在web.config中进行配置:
<customErrors mode="On" defaultRedirect="GenericErrorPage.htm"> <error statusCode="404" redirect="FileNotFound.htm" /> </customErrors>
如 果我们使用了IIS级别的Url Rewrite,我们不会配置Wildcard Mapping。也就是说我们只有在Rewrite之后的地址为aspx(或其他原本就该交由ASP.NET ISAPI处理)的情况下,ASP.NET引擎才会开始工作。如果用户请求了一个不存在的资源,那么404错误将由IIS发出,这时候404错误页面应该 在IIS里进行配置:
至此,有关URL Rewrite的话题已经讨论完了。在实际开发中肯定还会遇到各种各样不同的情况,但是只要理解了URL Rewrite方式的关键,按照程序运行的方式来思考,相信一般情况下不太会遇到难以处理的问题。