概要
分析如何使用微软提供的来对动态产生的URL地址进行网址重写 网址重写是实现一种截取网址请求并将其进行处理后重新指向到一个指定的网址的过程作者本人在对各种实现网址重写的技术进行研究和探讨后得出的经验和方法希望能对您有所帮助
内容简介
稍微花点时间看一看你做的网站里头的URL地址你看到类似这样的地址吗x?EmpID=&type=summary ?也许你会出于某种目的把大量的页面文件从一个目录甚至一个网站转移到其他地方而许多访问者出于个人兴趣或者研究目的之前就已经将原有网址收藏了起来 如果这时他从收藏夹打开该页面的时候发现这已经是坏链了本文旨在介绍如何使用网址重写将那些难看的网址转换成比较有实际意义的网址使其便于记忆例如将x?EmpID=&type=summary转换成如下地址 dispEmployeeInfo//l 我们甚至发现 网址重写技术可以解决令人头疼的错误或者说它可以创建一个智能化的错误解决方案
如上所述网址重写是实现一种截取网址请求并将其进行处理后重新指向到一个指定的网址的过程 在网址重写执行的期间相应处理程序处理被请求的网址从中提取出相关的值然后重新指向一个新的指定地址例如由于一次网站目录调整原有的 /people/ 子目录下的所有网页全部移动到/info/employees/目录原访问者从收藏夹或者其他什么地方点击链接发出访问/people/目录下的文件的请求时你肯定希望他还是能通过原有地址看到和原来相同的页面但实际上看到的却是网址重写指向的新目录下的相应文件
在老版本ASP中使用网址重写技术的途径很少要么写一个 ISAPI过滤器要么购买第三方厂商提供的网址重写组件然而在微软提供的ASPNET下你可以通过多种方法很简单地开发出自己的网址重写软件以满足自己各种不同的需要本文将和你一起讨论这门针对ASPNET开发人员的实现网址重写的技术然后举一些网址重写实际应用的例子在我们深入探讨网址重写技术的细节之前我们先看一下日常使用网址重写技术实现的场景
网址重写的一般用途
创建一个数据操作的ASPNET程序最常见的就是一个aspx页面后面带上一些查询参数集合例如在设计一个电子商务网站的时候假定你设计了一项功能允许用户浏览待售的商品为了更加方便操作你设计了一个页面displayCategoryaspx将商品按照给定的分类显示那么该分类下的商品显示页面上应该在页面文件对应网址后面加上了一个商品分类的查询参数例如用户要查询待售的装饰品在数据库中所有的装饰品数据对应的分类编号CategoryID的值为那么用户会访问如下网址x?CategoryID=
创建一个包含类似这样网址的网站最终有两种结果首先从最终用户的角度来观察x?CategoryID= 这个网址有些杂乱 可行性分析专家Jakob Neilson(主页 ) 建议选择网址显示方式时候考虑如下要求(参考网址)
· 是否简短
· 是否易于输入
· 是否将站点结构形象化
· 是否具有隐蔽性也就是让用户通过一个虚拟的看似有意义的导航地址访问指向该地址
我想还应该在上述列表中再增加一条 是否便于记忆x?CategoryID= 这个地址没有一个地方符合Neilson标准的任何一条也不便于记忆当然对于有经验的网络开发专家来说他们很熟悉这种键值对构成的查询参数结构体系然而对于普通用户来说输入这些带有参数的网址实在是太麻烦了
一种较好的方法就是使用一种比较直观且容易记忆的方式来将网址表示为 乍一看很容易就会推断这个网址所对应的内容极有可能会是显示装饰品(Widgets)信息这个网址就变得更加容易记忆和传播!然后我告诉我的同事请查看这个网址 不用我说第二遍她可能一次就把地址敲到浏览器上了(你也可以在亚马逊()的网站上这样尝试一下)很快就浏览器上就列出了装饰品(Widgets)的内容这里隐蔽性表示用户可以自行变更网址的结尾例如输入 就能看到全部分类相关的商品列表或者列出所有相关商品分类目录列表
注用上述简单的变更网址内容的方法来构思一下如今的比较流行的Blog网站生成的网址例如要查询年月日所发的帖子只需输入 即可如果将网址裁减为 则显示 年月份的帖子 同样将月份裁减掉得到 则显示出年全年所发的帖子
网址重写技术除了用于将复杂的网址简单化之外它还能用于处理因网站目录调整或者其他原因导致产生大量的无效链接和过期书签
当一个Web请求传送到IIS会发生什么?
在探讨如何实现网址重写这项技术之前很有必要了解一下IIS是处理所接收的Web请求的机制 当一个Web请求到达IIS Web服务器时IIS会根据所请求的文件后缀名来决定如何处理该请求IIS可以处理诸如HTML页面图片静态内容或者将请求转发给ISAPI应用程序由该ISAPI应用程序处理后生成HTML静态内容返回给IIS最后由IIS将请求结果发送回给客户端(一个ISAPI应用程序就是一套编译好能随时在后台运行的类库它的任务就是根据请求生成相关的内容)
例如如果IIS接收到一个对Infoasp的请求它会将该请求转交给 aspdll来处理该ISAPI应用程序调出并执行所请求的ASP页面然后把生成的HTML代码返回给IISIIS最后把内容发送回请求客户端对于ASPNET页面IIS则将请求转交给名为 aspnet_isapidll的ISAPI应用程序来处理该ISAPI应用程序 调用托管的ASPNET工作进程来处理该请求并将生成的HTML代码返回给请求客户端
你可以自定义IIS 将某一类扩展名映射到指定的ISAPI应用程序图一显示了IIS管理工具中的应用程序配置对话框
图一.已配置的文件扩展名映射
关于对IIS如何管理所接收的请求的详细探讨有些超出本文内容重要的是要了解 ASPNET引擎只负责处理对扩展名已经被正确配置映射到aspnet_isapidll的网络请求
用ISAPI过滤器来分析请求除了将请求的文件扩展名映射到相应的ISAPI应用程序外IIS还执行一些其他工作例如 IIS还主动对发出请求的客户端用户进行授权并判断已授权用户是否对其请求的文件拥有访问权限在一个请求过程的全部生命期内IIS的处理经历了几个阶段在每一个阶段IIS都生成一个事件而该事件可以被ISAPI过滤器实时操控的
如同ISAPI应用程序一样 ISAPI过滤器也是一块块安装在Web服务器上的非托管代码 ISAPI应用程序用于对所接收的特定文件类型做出响应而ISAPI过滤器含有对IIS生成的事件做出响应的代码(contain Code)甚至可以编辑进出的数据ISAPI也含有众多应用程序包括
· 权限控制与授权(Authentication and Authorization)
· 日志记录与监视(Logging and Monitoring)
· HTTP内容压缩(HTTP Compression)
· 网址重写(URL Rewriting)
本文所探讨的用ASPNET实现的网址重写技术就是 基于ISAPI过滤器用于网址重写的技术内容然而我们仍然要讨论一下究竟是使用ISAPI过滤器还是使用ASPNET应用程序提供的技术来实现网址重写技术
当一个请求传入ASPNET引擎的时候会发生什么?
ASPNET问世之前在IIS Web服务器上的网址重写功能需要通过ISAPI过滤器来实现自从这个家伙问世后我们就能通过ASPNET来实现URL重写了因为ASPNET的解释引擎与IIS有极大的相似之处产生这些相似性主要是因为 ASPNET
· 在处理接收的请求的生命期内也会产生事件
· 允许任意数量的HttpModule操控产生的事件这与IIS中的ISAPI过滤器类似
· 将请求的资源委托给HttpHandler处理这与IIS中的ISAPI应用程序类似
和IIS一样在一个请求的整个生命期内ASPNET对该请求的处理状态发出的状态改变信号引发相应的事件例如BeginRequest事件在ASPNET开始响应客户端请求之始引发AuthenticateRequest事件在ASPNET确立用户身份后引发当然还有诸如AuthorizeRequestResolveRequestCache和EndRequest等其它很多事件这些 都是SystemWebHttpApplication类下的事件更多信息请参考技术文档中的类HttpApplication概要
如上所述 可以创建ISAPI过滤器并用于相应IIS引发的事件同理ASPNET也提供了HttpModule用于响应ASPNET引擎引发的事件一个ASPNET应用程序 通过配置可以拥有多个HttpModuleASPNET引擎 每处理一个请求便初始化一个相应配置好的HttpModule并允许它 针对请求处理期间引发的事件生成相应的事件委托事实上ASPNET引擎 处理每一个请求调用大量的事件委托FormsAuthenticationModule就是众多内嵌HttpModule中的一个它 首先检查是否使用表单授权如果是的话它 将检查用户是否已授权如果没有授权则自动把用户重定向到指定的登录页面(即在中可以直接记录并判别用户登录授权的问题了!)
回忆在IIS中一项请求最后被转交给一个ISAPI应用程序处理该应用程序针对每一项请求进行处理并返回相应的数据例如客户端发出一个访问经典ASP页面的请求IIS将该请求转交给aspdll程序处理aspdll针对该请求执行asp页面内容并返回HTML编码ASPNET也使用了类似的手法ASPNET引擎在将这些HttpModule初始化后判断并决定调用相应的HttpModule来处理该请求(问怎么程序操作httpModule)
所有通过ASPNET引擎解析的请求最终被送交一个HttpHandler或者HttpHandlerFactory(一个HttpHandler只是简单地返回一个用于处理该请求的HttpHandler的实例)最终的委托呈现并响应所请求的HTML编码并发送回IISIIS则将HTML返回给请求客户端
ASPNET包含许多HttpHandler例如PageHandlerFactory是用于呈现ASPNET页面内容WebServiceHandlerFactory用于呈现ASPNET Web服务的SOAP数据包TraceHandler用于将ASPNET请求资源的HTML标记写入traceaxd
图二描绘了一个针对ASPNET资源的请求所经过的处理流程首先IIS接收到该请求并将其转交给aspnet_isapidll其次ASPNET引擎将一些HttpModule初始化最后最终的HttpHandler被调用生成相应的标记语言并将其返回给IIS最终返回到请求客户端
图二.IIS和ASPNET对请求的处理过程
创建并注册自定义HttpModule和HttpHandler
创建自定义HttpModule的工作相对较简单它包括一个实现当前接口的托管类HttpModule必须实现SystemWebIHttpModule接口同样HttpHandler和HttpHandlerFactory必须分别实现SystemWebIHttpHandler接口和SystemWebIhttpHandlerFactory接口有关创建HttpHandler和HttpModule的细节已经超出本书范围
一旦HttpModule和HttpHandler被创建后必须向Web应用程序注册如果要 向整个Web服务器HttpModule和HttpHandler只需简单的写入nfig文件如果是 由指定的Web应用程序调用则需在该程序的nfig配置文件中添加几行XML标记
例如要向指定的Web应用程序注册HttpModule和HttpHandler只需向该Web应程序的nfig配置文件中configuration\SystemWeb节中添加下列几行
<HttpModules>
<add type=type name=name />
</HttpModules>
其中type属性为HttpModule的标识号和类库名称name属性则为该模块取一个较为友好的名称方便 在Globalasax调用
HttpHandler和HttpHandlerFactory则是 在nfig文件中configuration\SystemWeb节中添加<httpHandler>标记例如
<httpHandlers>
<add verb=verb path=path type=type />
</HttpModules>
回忆上文 ASPNET对每一个接收到的请求指派相应的HttpHandler来处理并呈现相应内容该指派决定于所接收请求的verb和path的内容verb为HTTP请求的类型GET或者POSTpath则为请求的文件的路径和文件名如果我们打算用一个HttpHandler来处理所有GET类型和POST类型的并且文件扩展名为scott的内容可以在nfig相应配置节中加入下列标记
<httpHandlers>
<add varb=* path=scott type=type />
</httpHandlers>
其中 type是我们定义的HttpHandler的类型
注意在注册HttpHandler的时候必须注意HttpHandler所使用的文件扩展名必须已经在IIS中做指向ASPNET引擎的映射在上面scott扩展名的例子中如果我们所使用的scott扩展名如果没有在IIS中做指向ASPNET引擎的映射的话假定对fooscott文件发出请求该请求 将导致IIS将fooscott文件内容直接呈现给客户端为了能够让HttpHandler处理该请求必须将scott扩展名在IIS中做指向ASPNET引擎的映射之后IIS才能正确地将scott的请求转交给相应的HttpHandler
实现网址重写
网址重写技术不但可以在IIS Web服务器一级通过ISAPI过滤器实现而且还可以在ASPNET一级通过HttpModule或者HttpHandler实现本文主要关注在ASPNET一级实现网址重写技术所以此时不必关注在ISAPI应用程序中实现网址重写的技术细节而且有很多第三方厂商提供的ISAPI过滤器
构建网址重写引擎
在ASPNET中实现网址重写很简单 只需调用SystemWebHttpContext类的RewritePath()方法即可HttpContext类中包含有关于特定HTTP请求的HTTP规范信息ASPNET引擎每接收到一个特定请求后便针对该请求创建一个特定的实例这个类包含一些属性诸如 Request和Response属性分别提供对请求和响应的访问Application和Session属性提供对Application变量和Session变量的访问User属性提供对已授权用户信息的访问
在微软NET Framework 版本中RewritePath()方法接收一个新路径的简单字符串在其内部HttpContext类的RewritePath(string)方法内在地更新Request对象的路径和查询参数除了RewritePath(string)方法之外NET Framework 版还提供了另外一些重载版本其中一个重载版本接收三个输入字符串参数这种交替的重载形式不仅仅只是设置Request对象的路径和查询参数这些属性而是设置更深层的成员变量这些成员变量用于为 PhysicalPathPathInfoFilePath属性计算Request对象值
为了实现ASPNET中的网址重写我们需要创建一个HttpHandler和HttpModule用于
·根据请求的路径决定所需要重写的路径
·重写路径如果需要的话可以调用RewritePath方法
以前文所构建的那个站点为例可以通过/info/employeeaspx?empID=EmployeeID来访问每一个雇员的信息为了使这个网址更加地具有隐蔽性我们可能会使用更加容易理解的访问方式如/people/雇员名aspx这里就有了一个网址重写的案例当接收到对/people/ScottMitchellaspx的请求的时候我们就得使用网址重写使得对该页面的请求被重写指向到先前使用的/info/employee?EmpID=地址
使用HttpModule来调用网址重写在ASPNET一级来执行网址重写既可以使用HttpHandler也可以使用HttpModule当使用HttpModule的时候必须决定如果该网址需要被重写的话究竟应该在整个请求的生命周期期间的那一个点来使用乍一看着有些武断但是这个决定以重大而且微妙的方式影响到你的应用程序之所以作出对网址重写点的选择是因为内嵌的ASPNET HttpModule使用Request对象的属性值来完成自己的工作(回忆一下重写路径对Request对象的属性值的改变)这些内嵌HttpModule和相应事件的密切关系列举如下
HttpModule事件简介FormsAuthenticationModuleAuthenticateRequest判断用户是否已通过表单授权方式获取授权如果没有的话则将用户重定向到指定的登录页面FileAuthorizationModuleAuthorizeRequest当使用Windows授权方式的时候该HttpModule判断并确定该Microsoft Windows帐户是否对其请求的资源拥有足够的权限UrlAuthorizationModuleAuthorizeRequest检查并确认请求者是否对所访问的网址拥有权限该Url授权可以在nfig文件的<authorization>和<location>元素中配置
回想一下BeginRequest事件在AuthenticateRequest事件之前引发而AuthenticateRequest事件又在AuthorizeRequest事件之前引发
实现网址重写的一个较为安全的场合就是把它放在在BeginRequest事件中执行这意味着如果要执行网址重写的话在众多内嵌HttpModule运行的时候他已经完成了这种途径的最终用途淋漓尽致地体现在表单验证上当用户访问受限资源的时候 如果之前使用了表单验证他会自动被重定向到指定的登录页面在成功登录之后用户被重定向回先前试图访问的受限制页面
如果把网址重写放在BeginRequest事件或者AuthenticateRequest事件中在登录页面上执行提交后该页面会将用户重定向到网址重写指定的页面假定当用户在浏览器上敲入/people/ScottMitchellaspx地址该地址是要被重定向到/info/employeeaspx?EmpID=的 如果该Web应用程序设定使用表单验证当用户开始访问/people/ScottMitchellaspx的时候该网址将重写指向/info/employeeaspx?EmpID=接着ForumAuthenticationModule启动如果需要的话将用户重定向到登录页面用户登录后重定向到的页面将是/info/employeeaspx?EmpID=这也是自从FormAuthenticationModule启动运行时所发出请求的页面
同上类似当把网址重写放在BeginRequest事件或者AuthenticateRequest事件中运行的时候UrlAuthenticationModule也发现了网址重写指向的网址这意味着如果在该应用程序的nfig文件中<location>节为特定的网址配置特定的授权地址的话你得引用重写所指向的网址
为了解决这个微妙的问题一个可能就是把网址重写放在AuthorizeRequest事件中运行但是在使用这种方法解决URL授权和表单授权的异常时又引入了一个新的缺陷文件授权会失效当使用Windows验证的时候FileAuthorizationModule检查并验证已通过验证的用户是否拥有足够的权限访问特定的ASPNET页面
假定有一群用户并没有Windows级别的访问权限访问C:\inetpub\wwwroot\info\employeeaspx当这些用户试图访问/info/employeeaspx?EmpID=的时候他们会得到未授权的错误如果我们把网址重写放到AuthenticateRequest事件中运行当FileAuthorizationModule验证该安全性设置的时候他仍任人为被请求的文件是/people/ScottMitchellaspx而这时该网址已经被重写了因此FileAuthorizationModule会直接放行让用户看到了网址重写指向的内容/info/employeeaspx?Empid=
那么什么时候在HttpModule调用网址重写合适呢?他决定于所使用的验证方式当然如果不使用验证方式的话那么无论是在BeginRequest事件AuthenticateRequest事件还是AuthorizeRequest事件中调用网址重写没有多大区别如果使用表单验证方式并且不使用Windows验证方式的话把网址重写放入AuthorizeRequest事件委托中调用既可如果使用Windows验证方式的话把这项功能放入BeginRequest事件或者AuthenticateRequest事件调用就行了
使用HttpHandler来调用网址重写
除了上面所述方法外网址重写也可以放入HttpHandler或者HttpHandlerFactory中调用HttpHandler是一个负责针对特定请求生成相应内容的类而HttpHandlerFactory返回一个HTTP的实例该实例针对特定请求生成相应内容
本节将着眼于为这些ASPNET页面创建一个网址重写的HttpHandlerFactory创建HttpHandlerFactory必须实现IHTTPHandlerFactory接口它包括一个GetHandler()方法ASPNET引擎在初始化这些HttpModule后做出决定针对该请求调用相应的HttpHandler或者HttpHandlerFactory在调用HttpHandlerFactory的时候针对该Web请求以及随同的其他信息的HttpContext中经过的的HttpHandlerFactory的GetHandler()方法将被ASPNET引擎调用HttpHandlerFactory必须返回一个能委托该请求的对象并且该对象要能实现IHttpHandler接口
要通过一个HttpHandler来调用网址重写可以先创建一个HttpHandlerFactory它的GetHandler()方法检查所请求的网址并决定是否需要调用网址重写如果要调用网址重写的话则调用前文所述的已通过检查的HttpContext对象的RewritePath()方法最后该HttpHandlerFactory返回一个由类SystemWebUIPageParser的GetCompiledInstance()方法返回的HttpHandler(这与内嵌于ASPNET页面的HttpHandlerFactory(PageHandlerFactory)的工作原理相同)
在所有HttpModule被初始化后HttpHandlerFactory就开始被实例化把网址重写放在这些事件场所的最后一个里头调用的时候也会碰到相同的问题文件授权将会失效如果非要依赖于Windows验证和文件验证的时候你可能得使用HttpModule来调用网址重写了
下一章我们着眼于如何构建一个可重用的网址重写引擎使用下文所提的这些示例均以真实案例作为参照在作者主页上提供下载先用用一个简单的网址重写的例子来探讨如何实现网址重写紧接着将利用网址重写引擎中正则表达式的强大处理能力来展示真正隐蔽的网址重写技术!
使用网址重写引擎实现简单的网址重写
为了便于在Web应用程序中实现网址重写我构建了一个网址重写引擎该引擎提供下列功能
·可以在nfig文件中为页面开发者定义其所使用的网址重写引擎的规则
·通过使用正则表达式来使所制定的网址重写规则具有更加强大的重写能力
·能够通过简单配置即可在HttpModule和HttpHandler中使用网址重写
本节只探讨通过HttpModule来实现网址重写要了解如何通过HttpHandler来实现网址重写请下载本文提供的代码
设置网址重写引擎的配置信息
我们来探讨一下在nfig中网址重写规则的配置节首先必须在nfig文件中指出是否需要在HttpHandler或者HttpModule中调用网址重写在nfig中下文已经包含了两个已经被注释掉的配置节
<!
<HttpModules>
<add type=URLRewriterModuleRewriterURLRewriter name=ModuleRewriter/>
</HttpModules>
>
<!
<httpHandlers>
<add verb=* path=*aspx type=URLRewriterRewriterFactoryHandlerURLRewriter />
</httpHandlers>
>
被注释掉的<HttpModules>为配置使用HttpModule调用网址重写注释掉的<httpHandler>为配置使用HttpHandler调用网址重写
不论配置使用<HttpModules>还是<httpHandlers>调用网址重写除此之外还须配置网址重写规则一条重写规则包括两项字符串请求URL中的查找模式和针对该模式的匹配成功后的替换字符串该信息在nfig文件中用下列标签描述
<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>节要使用正则表达式来进行字符串匹配和替换(在此我们举一个例子来说明如何使用正则表达式来对字符串进行匹配和替换)既然该查找模式是一个正则表达式那么要注意避开对正则表达式保留字符串的直接使用(正则表达式的保留字符串包括有?^$等等可以通过在前面加上一个反斜线来引用这些保留字符例如\表示引用一个句点)
使用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
{
public virtual void Init(HttpApplication app) {
// WARNING! This does not work with Windows authentication!
// If you are using Windows authentication
// change to appBeginRequest
appAuthorizeRequest += new EventHandler(thisBaseModuleRewriter_AuthorizeRequest);
}
public virtual void Dispose() {}
protected virtual void BaseModuleRewriter_AuthorizeRequest(object sender EventArgs e) {
HttpApplication app = (HttpApplication) sender;
Rewrite(appRequestPath app);
}
protected abstract void Rewrite(string requestedPath HttpApplication app);
}
注意该BaseModuleRewriter类将网址重写放在AuthorizeRequest事件中调用如果要使用Windows验证并使用文件验证模式时请修改代码将网址授权放在BeginRequest或者AuthenticateRequest事件中
ModuleRewriter继承自BaseModuleRewriter并真正意义地实现了网址重写的操作该类仅包含一个重载了的方法Rewrite()其内容如下文所示
protected override void Rewrite(string requestedPath SystemWebHttpApplication app)
{
// get the configuration rules
RewriterRuleCollection rules = RewriterConfigurationGetConfig()Rules;
// iterate through each rule
for(int i = ; i < rulesCount; i++)
{
// get the pattern to look for and
// Resolve the Url (convert ~ into the appropriate directory)
string lookFor = ^ +
RewriterUtilsResolveUrl(appContextRequestApplicationPath rules[i]LookFor) + $;
// Create a regex (note that IgnoreCase is set)
Regex re = new Regex(lookFor RegexOptionsIgnoreCase);
// See if a match is found
if (reIsMatch(requestedPath))
{
// match found do any replacement needed
string sendToUrl = RewriterUtilsResolveUrl(appContextRequestApplicationPath reReplace(requestedPath rules[i]SendTo));
// Rewrite the URL
RewriterUtilsRewriteUrl(appContext sendToUrl);
break; // exit the for loop
}
}
}
该Rewriter()方法以获取nfig文件中的网址重写规则的设置为起始它通过循环访问各条网址重写规则每次均获取当前规则中的LookFor属性用正则表达式验证并判断是否查找是否对当前请求的网址是否有匹配
如果发现一条匹配将用当前规则的SendTo值对请求的路径执行一个正则表达式替换替换后的地址通过参数的形式传给RewriterUtilsRewriteUrl()方法RewriterUtils是一个帮助类它提供一对HttpModule和HttpHandler都可以使用的静态方法RewriterUrl()方法只是简单地调用了HttpContext对象的RewritePath()方法
注意你已经注意到了当执行正则表达式匹配和替换的时候调用了一个RewriterUtilsResolveUrl()方法该帮助方法简单地替换了应用程序路径中~的所有实例
我们已经探讨了主要的部分但是还有其它一些组件诸如将nfig文件中XML格式化了的网址重写规则反序列化至一个对象的类定义通过HttpHandlerFactory实现网址重写的类定义等本文最后三节将通过一些真实案例来探讨网址重写的技术
用网址重写引擎实现简单的网址重写
为了更好地示范网址重写引擎的运行我们来建立一个ASPNET Web应用程序来实现简单的网址重写引擎假定我们为一家在线销售各类商品的公司服务这些产品划分为以下类别
分类编号(CategoryID) 分类名称(CategoryName)
饮料(Beverages)
调味品(Condiments)
工艺品(Confections)
日记本(Diary Products)
假定已经建立好一个名为ListProductsByCategoryIDaspx的ASPNET页面文件它通过查询参数获取一个分类编号并根据此编号获取所有该分类下的所有商品如果用户想浏览所销售的饮料类商品可以通过ListProductsByCategoryIDaspx?CategoryID=来访问如果用户想浏览所销售的日记本类商品可以通过ListProductsByCategoryIDaspx?CategoryID=来访问假定还有一个页面ListCategoriesaspx它列出所有代售商品的分类编号
显然这里发现了一个网址重写的案例对于用户来说他们所输入的地址不具有任何实际意义并且不具备任何隐蔽性倒不如使用网址重写引擎让用户去访问/Products/Baverageaspx地址系统将该地址重写到ListProductsByCategoryIDaspx?CategoryID=我们可以在nfig文件中来完成网址重写任务
<RewriterConfig>
<Rules>
<! — Rules for products lister >
<RewriterRule>
<LookFor>~/Products/Baverageaspx</LookFor>
<SendTo>~/ListProductsByCategoryIDaspx?CategoryID=</SendTo>
</RewriterRule>
</Rules>
</RewriterConfig>
很明显地看到搜索用户访问的路径是否匹配/Products/Baverageaspx如果匹配的话则将网址重写到/ListProductsByCategoryIDaspx?CategoryID=
注意你会发现<LookFor>节点中避免直接在Baverageaspx中使用句点是因为<LookFor>节点的值是正则表达式的匹配模式在正则表达式中句点符号是一个特殊字符它表示匹配任何一个字符也就是说如果访问BaverageQaspx时也会发生匹配为了避免发生这个句点引起的匹配我们得在该句点符号前面加上一个\表示引用句点符号
通过该规则定义当用户访问/Products/Baverageaspx文件的时候他们将看到代售的饮料类商品列表信息图为访问/Products/Baverageaspx地址时的浏览器截图注意在浏览器中地址栏上显示的是用户输入的/Products/Baverageaspx地址但是实际访问的地址却是网址重写后的/ListProductsByCategoryIDaspx?CategoryID=(事实上在服务器上根本就不存在/Products/Baverageaspx文件!)
图三.网址重写后的对商品分类的请求
和/Products/Baverageaspx类似下一步我们添加其它分类的重写规则只需简单地在nfig文件中<Rules>中在添加其他<RewriteRule>节即可该演示完整的重写规则集合请参考下载文档的nfig文件中的定义
为了让该网址更具有隐蔽性如果让用户把/Products/Baverageaspx后面Baverageaspx一段截去在浏览器中输入/Products/来浏览产品分类列表会更好一些乍一看这项任务微不足道只需添加一条网址重写规则将/Products/映射到/ListCategoriesaspx即可然而这里有一个微妙之处你必须先创建一个/Products/目录并在里面放一个空文件Defaultaspx
要认识为什么这些额外的步骤是必须的先回顾一下前文网址重写引擎是处于ASPNET一级的也就是说如果ASPNET没有获得处理请求的机会的话网址重写引擎就不能对输入的网址请求作出判断此外IIS仅在请求文件包含相应扩展名时才将请求转交给ASPNET引擎如果用户访问/Products/IIS并不知道其扩展名是什么于是它检查该目录下的文件看是否包含有默认首页文件名(DefaultaspxDefaultasp等等这些文件名在IIS管理工具对话框中Web服务器属性对话框中的文档标签中定义)当然如果/Products/目录不存在的话IIS将返回一个HTTP 错误
所以我们需要创建一个/Products/目录并在该目录下额外创建一个空文件DefaultaspxIIS会检查该目录下的文件发现有一个默认文件名Defaultaspx于是将请求转交给ASPNET这样网址重写引擎才能生效
<RewriterRule>
<LookFor>~/Products/Defaultaspx</LookFor>
<SendTo>~ListCategoriesaspx</SendTo>
</RewriterRule>
通过该规则用户访问/Products/Defaultaspx或者访问/Products/都可以看到如图四所示的产品分类列表
图四在网址上添加隐蔽性
处理回送数据
如果要重写的网址上包含有服务器端Web Form并执行数据回送当该Web Form回送数据时会暴露出真实的网址也就是说当用户访问/Products/Baverageaspx时浏览器上地址栏显示的也是/Products/Baverageaspx但是实际上是访问/ListProdutsByCategoryIDaspx?CategoryID=的内容如果ListProductsByCategoryIDaspx页面执行了数据回送的话用户被数据回送定向给原始的/ListProductByCategoryIDaspx?CategoryID=页面上而不是/Products/Baverageaspx页面这虽然不是什么大问题但是用户会觉察到点击一个按钮时网址发生了的变化这也许会令人不安因为如果出于网址安全的角度来说直接把真实的网址暴露出来了
之所以发生这种现象的原因是当Web Form在呈现之时就明确地设置其action属性为当前Request对象中文件路径的值当然在Web Form呈现之时从/Produts/Baverageaspx到/ListProductsByCategoryIDaspx?CategoryID=的网址重写就已经执行完毕了这意味着Request对象所汇报的是当前用户所访问的地址是/ListProductsByCategoryIDaspx?CategoryID=这么看来只需让该服务器端表单在呈现之时不呈现action属性即可解决问题了(对浏览器来说如果不设置action属性的话那么在提交的时候将使用其默认值)
然而不幸的是该Web Form不会允许你指定action属性也不会允许你通过设置一些属性来达到禁用呈现action属性的目的得自行继承SystemWebHtmlControlsHtmlForm这个类并重载该类的RenderAttribute()方法明确指出该类不呈现acton属性
感谢继承这个强大的功能使得我们很简单就获取了HtmlForm这个类下所有的功能定义只需少量几行代码就达到所需目的完整代码如下所示
namespace ActionlessForm
{
public class Form:SystemWebUIHtmlControlsHtmlForm
{
protected override void RenderAttributes(SystemWebUIHtmlTextWriter writer)
{
writerWriteAttribute(namethisName);
baseAttributesRemove(name);
writerWriteAttribute(methodthisMethod);
baseAttributesRemove(method);
thisAttributesRender(writer);
baseAttributesRemove(action);
if (baseID!=null)
{
writerWriteAttribute(idthisClientID);
}
}
}
}
对RenderAttributes()方法重载的代码包含了原类HtmlForm的RenderAttributes()方法全部的代码内容只是简单地去掉了设置action属性这一节
当创建并编译了这个类后将其添加到引用目录即可在该ASPNET Web应用程序中使用为了将原有HtmlForm类替换只需简单地在页面顶部添加下列代码
<% @ Register TagPrefix = skm Namespace = ActionlessForm Assembly = ActionlessForm %>
然后将<Form runat=server>标签替换为
<skm:Form id=Form method=post runat=Server>
并将结束标记</Form>替换为
<skm:Form>
你可以查看该文档相关下载中的ListProductsByCategoryIDaspx文件中的自定义Web Form该下载已经提供了完整的Visual StudioNET项目文件包
注意如果你打算进行网址重写的地址不执行数据回送则没有必要使用该自定义Web Form的类
创建真正隐蔽的网址
上一节简单网址重写的示例展示了如何通过新的网址重写规则来轻松地配置网址重写引擎本节将通过出众的正则表达式来展示网址重写的强大威力
时下正在流行Blog很多人都拥有一个自己的Blog不论你是否对Blog感到陌生他们正在不断地更新自己的Blog页面这些页面就像一个个人日记本一样大多数Bloger只是简单地记录每天发生的事情也有一些聚焦于某一主题比如影评球迷组织电脑技术等
Blog可以在任何地点由作者进行更新更新次数可以是一天多次也可以是一周一两次在Blog页面上只显示最近条更新但事实上所有的Blog软件都提供了存档记录访客可以阅读其历史记录有了隐蔽的网址Blog应用程序将变得更加强大假定你通过///aspx来查询自己的Blog上的文章你会为阅读到年月日的Blog感到惊讶吗?此外你可能为了访问年月所有的Blog而将该地址裁减为///要访问年所有的Blog你可能会试着去访问//
在维护一个Blog的时候如果将这种具有隐蔽性的网址提供给用户将会更好实际上很多Blog引擎都提供了这种网址重写的功能现在来看看这些是如何通过网址重写实现的
首先我们需要一个页面能够分别按照年月日分别显示Blog的内容假定现在已经做好了一个页面文件ShowBlogContentaspx它能分别获取年月日的查询参数要查看年月日所发的帖子我们可以访问/ShowBlogContentaspx?year=&month=&day=要浏览年月的数据可以访问/ShowBlogContentaspx?year=&month=要查询年所有数据可以访问/ShowBlogContentaspx?year=(在下载文件中提供ShowBlogContentaspx源代码)
然后当用户访问///aspx时我们需要将他访问的网址重写到/ShowBlogContentaspx?year=&month=&day=上这里需要制定三条网址重写规则当指定访问年月日时当指定访问年月时和当指定访问年时
<RewriterConfig>
<Rules>
<! Rules for Blog Content Displayer >
<RewriterRule>
<LookFor>~/(d{})/(d{})/(d{})aspx</LookFor>
<SendTo>~/ShowBlogContentaspx?year=$&month=$&day=$</SendTo>
</RewriterRule>
<RewriterRule>
<LookFor>~/(d{})/(d{})/Defaultaspx</LookFor>
<SendTo><![CDATA[~/ShowBlogContentaspx?year=$&month=$]]></SendTo>
</RewriterRule>
<RewriterRule>
<LookFor>~/(d{})/Defaultaspx</LookFor>
<SendTo>~/ShowBlogContentaspx?year=$</SendTo>
</RewriterRule>
</Rules>
</RewriterConfig>
这些网址重写规则展示了正则表达式的强大威力第一条规则按照(\d{})/(\d{})/(\d{})\aspx模式进行查找通俗的说它查找是否包含匹配xxxx/xx/xxaspx格式的字符串其中x表示数字每一组数字必须用圆括号括起来这样可以在相应<SendTo>节内引用圆括号内的匹配字符串我们可以使用$$$来分别引用前面匹配的圆括号组其中$$$分别表示所匹配的第一第二第三个圆括号组
注意由于nfig是XML格式的文档所以在文本域内必须回避直接使用一些特殊字符如&<和>符号等在第一条网址重写规则的<SendTo>节中用&来表示引用&符号在第二条网址重写规则的<SendTo>节中用<![CDATA[]]>元素来表示其中所有的内容都是文本域不再需要用转义字符来表示引用这两种方法都可以实现同样的目的
下面图五图六图七都显示出网址重写的运行状况这些数据都真实地摘自作者的Blog图五显示年月日的帖子图六显示所有年月的帖子图七显示年所有帖子
图五显示年月日的帖子
图六 显示年月所有的帖子
图七 显示年所有的帖子
注意要使用网址重写引擎强烈推荐在<LookFor>节中使用正则表达式
创建必须的目录结构
当IIS接收到对///aspx的请求时他发现文件扩展名aspx便将该请求转交给ASPNET引擎处理在ASPNET 引擎中传递时该地址被重写到/ShowBlogContentaspx?year=&month=&day=最后用户将看到该Blog上年月日所有的帖子但是在用户访问///时会发生什么呢?除非已经存在一个///的目录否则IIS将返回一个错误而且该目录下还必须要有一个默认页面DefaultaspxIIS才能将请求转交给ASPNET引擎处理
通过这种方法你得手动为每一年的Blog创建一个年份的目录并在该年份下放置一个默认文件Defaultaspx而且还得在该年份目录下创建每一月的目录从每一个目录下也要防止一个默认文件Defaultaspx(回想前面的例子为了将/Products/重写到/ListCategoriesaspx也是要建立一个/Products/目录并放置一个默认Defaultaspx文件
很明显这样创建目录结构的过程是很痛苦的解决这种问题的一个办法就是设置IIS将所有接收的请求都转交给ASPNET引擎来处理这种方法甚至连访问这种地址///IIS都如实地将其转交给ASPNET引擎处理这种方法造成ASPNET引擎得处理所有传入的请求包括css文件图片文件Javascript文件以及Flash文件等等
关于对所有类型文件的处理的详细讨论已经超出了本书范围有关在ASPNET Web应用程序中使用这些技术的例子请访问Text 这个开源的BlogText 可以通过配置将所有请求都转交给ASPNET处理它使用了一个自定义的HttpHandler来处理所有类型的文件类型这个自定义的HttpHandler可以识别并判断如何处理所有的文件类型(图像文件CSS文件等等)
结束语
本文探讨了通过类HttpContext类的RewriteUrl()方法来实现ASPNET一级的网址重写正如我们所看到那样RewriteUrl()方法在修改这个特有的HttpContext的Request的属性时也修改了所请求的文件和路径实际得到的效果就是在用户访问其特有的网址的时候他实际却是在服务器端请求另一个与此不同的网址
网址重写不但可以在HttpModule中执行也可以在HttpHandler中运行本文我们探讨了在一个HttpModule中执行网址重写也研究了一下网址重写在ASPNET中的各个不同场所的情况
当然在ASPNET一级的网址重写中只有在IIS成功地将请求转交给ASPNET引擎后才能成功地执行当用户请求一个扩展名为aspx的文件时这很自然地发生然而如果要让用户输入一个实际并不存在的网址通过网址重写到另一个存在的aspx页面你必须为该请求创建相应的目录和默认的Defaultaspx页面除非配置IIS让它把所有的请求都转交给IIS处理但是这种方式盲目地将所有请求都转交给了ASPNET引擎