大约一年半前,我在博客上写过一系列关于URL Rewrite
的文章(2、3、4),把ASP.NET
平台上进行URL Rewrite
的方式和各自地特点进行了较为详细的描述。应该来说,已经讲的非常具体,可以应对90%
的情况。其实IIS Rewrite
的原理非常容易理解,进行一些简单的变化和推断之后,便可以得出一些问题的原因和解决方案。现在我们就来看一个真实案例:在ASP.NET MVC
中使用IIS级别的URL Rewrite
。
在当时的文章中我谈到,URL Rewrite
分有IIS
级别和ASP.NET
两种级别,并且各有各的特点和限制。在ASP.NET MVC
中我们常用的方式是ASP.NET
级别的URL Routing
,它的作用是从URL
中捕获数据并交给程序使用(当然还有“构造”的功能,稍候再谈)。因此,在ASP.NET MVC
中我们往往不需要使用ASP.NET
级别的URL Rewrite
。而如今使用IIS级别的URL Rewrite
,也正是因为有某些特殊问题无法回避才“不得已而为之”的。
以下涉及到的URL
都以http://51programming.com
为例,这个域名已经被我泛解析为127.0.0.1
,如果您需要的话可以用它来做实验。
在许多年前,一个URL
的Path
就是普通的路径,而动态的参数,如查询路径,是通过Query String
提供的,例如:
http://51programming.com/products?keywords=helloworld
为了避免混淆,在这里我们先来澄清一些概念。什么是URL
,什么是Path
,而什么是QueryString
。例如在上面的地址,这三者分别是:
- URL:
http://51programming.com/products?keywords=helloworld
- Path:
http://51programming.com/products
- Query String:
keywords=helloworld
后来SEO兴起之后,有人说这样的“动态地址”不利于搜索引擎中的权重优化,因此建议把关键字作为Path
的一部分。于是就出现了这样的URL
:
http://51programming.com/products/helloworld
这么看来问题并不大,但是您要知道,关键字往往是由用户输入的,可能会输入特殊字符。例如,如果用户输入了“200%”作为关键字,则两种形式下的URL
就分别是:
http://51programming.com/products?keywords=200%25
http://51programming.com/products/200%25
如果您尝试一下便可以知道,第一个URL
可以正常访问,而第二个URL
便会引发Bad Request
异常:
这是因为URL的Path
部分出现了特殊字符,而这种字符只能出现在Query String
中。
看到这个画面,您还意识到了什么信息?在定位问题的原因,以及设法解决问题的时候,首先要明确的是到底是哪里出现了问题。例如看到这个画面,您应该清楚地意识到一点:这是ASP.NET
抛出的异常,换句话说,IIS
并没有把它当作是非法的URL
,它还是老老实实地将URL
交给ASP.NET ISAPI
处理。因此,我们便可以动用IIS
级别的URL Rewrite
,在进入ASP.NET
执行引擎之前,就把URL替换成可接受的形式:
RewriteRule ^/products/([^\?]*)\?(.+) /products?$2&keywords=$1 [I,L,U]
RewriteRule ^/products/([^\?]*) /products?keywords=$1 [I,L,U]
第一行应对的是带有Query String
的情况,而第二行则是没有Query String
的情况。这里用到的组件是IIRF(Ionic's Isapi Rewrite Filter)
,这是一款开源产品,一年半前的文章里我推荐的也是这个,现在它已经有了升级。它的功能便是在进入ASP.NET ISAPI
之前,就将URL
重写为其他形式:
原本在第3步会出现的Bad Request
,由于已经在第2步被URL Rewrite
成合法的形式。因此剩余的处理也就没有任何问题了。
这些内容在一年半前的文章内已经提过,不过现在既然有了ASP.NET MVC
,则事情又变得更为复杂。因为ASP.NET Routing
除了“匹配”URL的功能之外,还担负着“组装”URL的职责。因此,让ASP.NET Routing
能够识别出Rewrite
后的URL
不难,但是如何同时让它又可以“组装”出Rewrite
前的URL
,这就需要一些小技巧了。例如以下的Route
配置只能识别出URL输入(/products?keywords=xxx
)但不能组装出我们需要的URL(/products/xxx
):
routes.MapRoute(
"Product.List",
"products",
new { controller = "Product", action = "List" });
因此,我们必须这么做:
routes.MapRoute(
"Product.List",
"products/{*keywords}",
new { controller = "Product", action = "List", keywords = "" });
请注意我们让keywords
匹配Path
后端全部内容,而由于我们又提供了keywords
的默认值,因此即使是“/products
”这样的Path
输入,也能正确匹配到这条Route
规则——只不过此时的Route Value
中的keywords
字段已经不是用户输入的内容了(因为用户输入的/products/xxx
,已经被重写为/products?keywords=xxx
)。换句话说,如果有如下的Action
,那么它的keywords
参数则永远是空字符串:
public ActionResult List(string keywords) { ... }
幸好,ASP.NET MVC中存在Model Binder
机制,我们可以编写一个Model Binder
来指定这个参数的获取位置:
public class FromQueryBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
return controllerContext.HttpContext.Request.QueryString[bindingContext.ModelName];
}
}
再将其运用到List
的keywords
参数上去:
public ActionResult List(
[ModelBinder(typeof(FromQueryBinder))]string keywords)
由于参数名是keywords
,因此bindingContext.ModelName
也是keywords
,于是从Query String
中便可以取到我们需要的内容了。至于在进行URL
生成的时候,我们还是可以之间一样添加一个keywords
字段到Route Value
中去,于是在我们先前配置的Route
规则中便会组装成合适的Path
了(即/products/xxx
)。
在这个例子中,我们让keywords
匹配Path
后端全部内容,但是如果是Path
中间某一段需要有特殊字符怎么办呢?其实也一样,只是在进行URL Rewrite
的时候,需要在最终重写的时候填写一个“假”的值就可以了,如这样的Route
规则:
routes.MapRoute(
"Product.List",
"products/{keywords}/page",
new { controller = "Product", action = "List" });
而IIS级别的URL Rewrite
重写的规则就可以是:
RewriteRule ^/products/([^/]*)/(.*) /products/useless-segement/$2?keywords=$1 [I,L,U]
这样,如果用户输入/products/xxx/2
就会被重写成/products/useless-token/2?keywords=xxx
——事实上,在第一个示例中我们也可以这么做,只是我“不习惯”增加一个伪造的值而已。
以上解决方案可以在IIS 6
与IIS 7
的Classic Mode
中正常使用,只可惜在IIS 7
的Intergrated Mode
中,可能是由于ASP.NET
接管了IIS
的部分逻辑,因此会很早抛出“IIS级别”,而不是“ASP.NET级别”的Bad Request
异常。如果您遇到了这种方式,就必须通过以下三个步骤来摆脱这个麻烦的问题了:
- 设置
AllowRestrictedChars
:KB820129(让IIS 7接受特殊字符) - 设置
VerificationCompatibility
:KB826437中除了“安装.NET 1.1 SP1
”以外的步骤(让ASP.NET
接受特殊字符) - 将ASP.NET页面的
ValidateRequest
设为False
其实您只要经过了这三步修改,对于目前这个案例,即使不用IIS
级别的URL Rewrite
应该也没有问题了。