Hello World
Spiga

在ASP.NET MVC中使用IIS级别的URL Rewrite

2009-09-23 15:03 by 老赵, 14025 visits

大约一年半前,我在博客上写过一系列关于URL Rewrite的文章234),把ASP.NET平台上进行URL Rewrit的方式和各自地特点进行了较为详细的描述。应该来说,已经讲的非常具体,可以应对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应该也没有问题了。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

23 条回复

  1. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-09-23 15:07:00

    谁敢抢我的沙发?

  2. 我是新手呵呵啊[未注册用户]
    *.*.*.*
    链接

    我是新手呵呵啊[未注册用户] 2009-09-23 15:14:00

    这个很实用 不错

  3. JimLiu
    *.*.*.*
    链接

    JimLiu 2009-09-23 15:27:00

    @麒麟.NET
    你抢的不是沙发,是寂寞

  4. 老赵
    admin
    链接

    老赵 2009-09-23 15:50:00

    某些带脏字回复已删除。

  5. 强沙发的看到一次骂一次[未注册用户]
    *.*.*.*
    链接

    强沙发的看到一次骂一次[未注册用户] 2009-09-23 15:58:00

    删除没问题,只是想治治那些整天强沙发的无聊的家伙

  6. 老赵
    admin
    链接

    老赵 2009-09-23 16:04:00

    @强沙发的看到一次骂一次
    嗯,抢沙发的确没什么意思。
    多谢大家支持,不过大家还是多关注文章内容本身吧,呵呵。

  7. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-09-23 16:13:00

    @强沙发的看到一次骂一次
    我是看完文章刷新一遍看没有留言才抢的,玩玩而已,何必当真?

  8. 妖居
    *.*.*.*
    链接

    妖居 2009-09-23 16:43:00

    @麒麟.NET
    抢沙发的同时学习一下,无所谓了。要不就做个“老赵博客抢沙发专用工具”,读取RSS信息,发现新的了就自动回复一个。

    @Jeffrey Zhao
    已经用MVC开发了一个系统,真实的系统。现在是收尾工作。但是老赵提出的很多问题,比如这篇说的,还有此前的页面缓存,我都没有在项目中遇到(或者说项目还没有需要用到这些的需求)。老赵的项目都多大啊?访问量有多少?需要这些各种各样的技术来实现。

  9. 老赵
    admin
    链接

    老赵 2009-09-23 16:51:00

    @妖居
    我现在做的项目还没有发布,目标是很大了……
    不过,我构造的这些东西不是根据大小来的啊,是根据业务和当前条件来的。
    例如这个URL问题,是URL使用策略决定的。
    例如缓存,是发现旧有系统上的一些实现策略没法运用到MVC上才自己做的。
    还有很多,其实是一些“编程”实践决定的,例如为了简化开发等等。

  10. Jeffrey Zhao 的fans[未注册用户…
    *.*.*.*
    链接

    Jeffrey Zhao 的fans[未注册用户] 2009-09-23 17:43:00

    老大,别写那么快呀。跟不上。

  11. 见过手淫了,是上周六,票房很火[未注册用户…
    *.*.*.*
    链接

    见过手淫了,是上周六,票房很火[未注册用户] 2009-09-23 21:24:00

    在ASP.NET MVC中我们常用的方式是ASP.NET级别的URL Routing,它的作用是从URL中捕获数据并交给程序使用(当然还有“构造”的功能,稍候再谈)。因此,在ASP.NET MVC中我们往往不需要使用ASP.NET级别的URL Rewrite。
    这句话的因此,不知道为什么来的因此。

  12. 老赵
    admin
    链接

    老赵 2009-09-23 22:21:00

    @见过手淫了,是上周六,票房很火
    因为ASP.NET URL Routing的作用是直接处理URL,而URL Rewrite的功能是将一种URL变成另一种可供处理的URL的形式。
    这里可能没说清,多谢提醒!

  13. Kevin Dai
    *.*.*.*
    链接

    Kevin Dai 2009-09-23 22:30:00

    时间可真快,一年半又过去了,我还记得那几篇文章,还记得有关老赵为WebForm平反的几篇。

  14. 老赵
    admin
    链接

    老赵 2009-09-23 22:43:00

    @Kevin Dai
    不管用什么,我都用自己认为最合适的做法。
    微软的东西是好的,但是看着微软的广告用,很可能用不好。

  15. Next.Ko
    *.*.*.*
    链接

    Next.Ko 2009-09-23 23:54:00

    用户提交时把200%换成200p来构成地址行不?

  16. 老赵
    admin
    链接

    老赵 2009-09-24 00:31:00

    @Next.Ko
    那用户真正希望输入200p的时候又该怎么办呢?

  17. nk.....[未注册用户]
    *.*.*.*
    链接

    nk.....[未注册用户] 2009-09-24 10:19:00

    看来200p还是不行...

  18. 老赵
    admin
    链接

    老赵 2009-09-24 10:32:00

    @nk.....
    是啊,“转义”不是那么简单的……

  19. yankai
    *.*.*.*
    链接

    yankai 2009-09-24 10:33:00

    看了2遍才看明白了。不过感觉老赵把问题搞复杂了。

  20. B.e.e[未注册用户]
    *.*.*.*
    链接

    B.e.e[未注册用户] 2009-09-25 12:55:00

    谢谢老赵!!!

    这篇正解决了之前我在你的Q&A里提的问题!!!

  21. 老赵
    admin
    链接

    老赵 2009-09-25 13:18:00

    @yankai
    有什么简单的做法?

  22. c#gavin
    *.*.*.*
    链接

    c#gavin 2009-10-23 17:21:00

    打扰了。没有找到Northwind,

  23. 链接

    Bob Dong 2010-04-23 20:22:39

    呀 老赵你好! 看你很多文章了,

    我今天遇到了MVC URL 参数特殊字符。

    找了好多方法没有解决。 看到你这个文章很是深有启发,但是IIS不是咱能控制的,装什么插件啊 不可能。

    所以还是用传统的传参了。

    不知道ASP。net MVC 2 有没有简单的处理

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我