Hello World
Spiga

重提URL Rewrite(3):在URL Rewrite后保持PostBack地址

2008-01-13 03:17 by 老赵, 18773 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 action="/User.aspx?name=jeffz" />,这是因为在生成代码时,页面会使用当前Request.Url.PathAndQuery的值来得到form元素的action。这导致了一旦PostBack,地址栏里就会出现“User.aspx?name=jeffz”,而这个地址很可能是请求不到正确的资源的(因为可能被Rewrite到了别处,或者由于目录级别的关系而根本没有该资源)。在之前《UpdatePanel与UrlRewrite》一文中,我说可以在页面末尾添加一行JavaScript代码来解决这个问题:

<script language="javascript" type="text/javascript">
    document.getElementsByTagName("form")[0].action = window.location;
</script>

这行代码的意图非常明显,将form的action修改为window.location(即浏览器地址栏中的路径),这样当页面进行PostBack时,目标地址就会是URL Rewrite之前的地址了。这种做法能够让程序正常运行,但是实在不能让我满意。为什么?

因为太丑了。

因为我们还是把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其实一直在等待“action”这个属性被输出的那一刻,将value变为当前Request对象的RawUrl属性。这个属性在ASP.NET刚接受到IIS传来的请求时就确定了,它不会随着接下来BeginRequest中的Rewrite操作而改变,因此我们只要为Form的action输出RawUrl就可以解决PostBack地址改变这个问题了。

不过要让这个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);
}

现在action的value已经不是简单地从RawUrl属性中获取了,而是设法从ServerVariables集合中取得HTTP_X_REWRITE_URL变量的值,因为那里存放了IIS所接受到的原始请求的地址。

至此,有关URL Rewrite的主要话题已经讲完了,在下一篇,也就是本系列的最后一篇文章中,我们将重点看一下使用不同层面的URL Rewrite会在一些细节方面造成什么样的区别,以及相关的注意点。

相关链接:

(1)IIS与ASP.NET

(2)使用已有组件进行URL Rewrite

(4)不同级别URL Rewrite的一些细节与特点

Creative Commons License

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

Add your comment

53 条回复

  1. 韩现龙
    *.*.*.*
    链接

    韩现龙 2008-01-13 08:03:00

    好人啊老赵!
    虽然还没仔细看这篇文章,可是已经知道它就是我昨天在思考问题的解决方案了。

  2. 丁学
    *.*.*.*
    链接

    丁学 2008-01-13 10:19:00

    呵呵,Control Adaptor是个好东西啊

  3. SZW
    *.*.*.*
    链接

    SZW 2008-01-13 10:42:00

    学习学习
    这个应该可以很好地解决DiscuzNT这方面的问题

  4. 海风吹呀吹
    *.*.*.*
    链接

    海风吹呀吹 2008-01-13 10:49:00

    老赵的文章就是不一样!哈哈!URL Rewrite实现URL重写的确是实现静态的一个理想解决方案!apache不用多讨论了,相信大家都知道了它的强大,但是在IIS下面,实现apache的功能,恐怕非ISAPI_Rewrite莫属(虽然不是微软的东西),基于正则的ISAPI_Rewrite的优先解析比.NET自带的那个理论上应该要快吧?如果老赵对ISAPI_Rewrite熟悉,强烈要求老赵为新手们写一下关于ISAPI_Rewrite有关实现无限二级域名的文章,特别是ISAPI_Rewrite如何让二级域名保持不变,这个问题一直是困扰新手们很久的问题。(比方说http://JeffreyZhao.cnblogs.com/点了回车之后URL地址不要变)

  5. 海风吹呀吹
    *.*.*.*
    链接

    海风吹呀吹 2008-01-13 11:28:00

    需要美刀不可怕,呵呵,偶已经买了一个

  6. Goumh
    *.*.*.*
    链接

    Goumh 2008-01-13 11:33:00

    好文章,学习.......

  7. Zero One[未注册用户]
    *.*.*.*
    链接

    Zero One[未注册用户] 2008-01-13 11:57:00

    思路清晰,介绍系统,好文章

  8. 5yplan[未注册用户]
    *.*.*.*
    链接

    5yplan[未注册用户] 2008-01-13 12:34:00

    精力充沛呀~算的上是一日三篇了。呵呵

  9. 老赵
    admin
    链接

    老赵 2008-01-13 12:41:00

    @5yplan
    平时没时间写,有机会多写几篇,呵呵。

  10. 江大鱼
    *.*.*.*
    链接

    江大鱼 2008-01-13 14:05:00

    叶面load完成之后再把url再rewrite回去也是一个不错的方法,

    http://www.cnblogs.com/jzywh/archive/2007/12/20/urlrewriteaction.html

  11. SZW
    *.*.*.*
    链接

    SZW 2008-01-13 14:05:00

    @Jeffrey Zhao
    有没有办法可以同样利用正则表达式,获取~/User.aspx?name=$1翻译成~/User/[name](这个应该不是问题),然后在页面生成的时候,加到form/server的action中呢?可以的话这样就可以在ASP.NET内部把这个问题处理掉了。

  12. 老赵
    admin
    链接

    老赵 2008-01-13 14:07:00

    @SZW
    肯定可以阿,我们可以把前者Rewrite到后者,自然可以反过来,呵呵。

  13. 老赵
    admin
    链接

    老赵 2008-01-13 14:08:00

    @江大鱼
    你的页面被提示有木马……

  14. SZW
    *.*.*.*
    链接

    SZW 2008-01-13 14:14:00

    @Jeffrey Zhao
    那么做不是可以把问题简化很多吗?江大鱼提供的例子似乎有点符合这个意思,不过我还没测试。

  15. 老赵
    admin
    链接

    老赵 2008-01-13 15:33:00

    @SZW
    我没有理解你的意思,什么东西会简化很多?

  16. Leem
    *.*.*.*
    链接

    Leem 2008-01-13 15:44:00

    --引用--------------------------------------------------
    SZW: @Jeffrey Zhao
    那么做不是可以把问题简化很多吗?江大鱼提供的例子似乎有点符合这个意思,不过我还没测试。
    --------------------------------------------------------
    Adaptor这么优雅的做法你不用,为什么一定要自己在页面里处理呢.

  17. SZW
    *.*.*.*
    链接

    SZW 2008-01-13 15:56:00

    @Leem
    Adaptor是很好,老赵的方法我也正在测试用到一些地方,我只是提出一种另外一种做法的可能,另外按照江大鱼给的例子来看,也不是每个页面都需要一对一地处理,它是基于basepage的,所以“优雅”一点上来说,页面和不页面已经不是重点。

  18. Argo
    *.*.*.*
    链接

    Argo 2008-01-13 16:06:00

    这个Intelligencia.UrlRewriter内就已经有对ControlAdapter的支持了,只需要如下配置Browser文件即可。

    <

  19. 老赵
    admin
    链接

    老赵 2008-01-13 16:27:00

    @SZW
    嗯,如果写在BasePage里可能也不错吧。不过这个方法相对于Adaptor来说侵入性还是高了点,如果一个页面又没有Form那么怎么办呢?用Adaptor的话,相当于将这部分逻辑提取出来了,而且是面向Form本身,而不是页面。
    江大鱼的例子可能也会造成问题,OnLoadComplete后面还有很多生命周期的过程呢,万一某个地方有依赖的话……所以相比起来我还是觉得Adaptor更好。

  20. 老赵
    admin
    链接

    老赵 2008-01-13 16:27:00

    @Argo
    多谢补充。:)

  21. 韩现龙
    *.*.*.*
    链接

    韩现龙 2008-01-13 16:42:00

    刚一不小心发现一个问题!!
    情景如下:
    这是我的重写规则之一:


    当我输入"list1.htm"时页面能够正常显示,可如果输入"list1.htm/"时页面就无法正常显示了~~样式什么的都没了,而且在IE7下提示"Stack overflow at line:0 ",在FF下是“正在从localhost加载数据”和“等待...."轮换显示。
    老赵,这是怎么回事啊?

  22. 韩现龙
    *.*.*.*
    链接

    韩现龙 2008-01-13 16:43:00

    url="~/list(.+).htm" to="~/frmNewsList.aspx?id=$1" processing="stop"

    上面是我的重写规则。
    晕,尖括号没显示出来。

  23. 老赵
    admin
    链接

    老赵 2008-01-13 16:56:00

    @韩现龙
    url="^/List(.+)\.htm$"

  24. 韩现龙
    *.*.*.*
    链接

    韩现龙 2008-01-13 17:00:00

    @Jeffrey Zhao
    。。竟然忘了是正则的事

  25. airwolf2026
    *.*.*.*
    链接

    airwolf2026 2008-01-13 18:47:00

    学习了.以前一直纳闷的类似/user/Contact.do可以打开一个html页面的迷惑总算解开了.

  26. MK2
    *.*.*.*
    链接

    MK2 2008-01-13 21:12:00

    呵呵,感谢老赵的好文,在UrlRewriter.NET中也发现了该作者也对Form的Action问题改写了Form类。

    呵呵,继续收藏剩下的一篇。

  27. 一路走好
    *.*.*.*
    链接

    一路走好 2008-01-14 16:42:00

    學習

  28. 蓝天旭日
    *.*.*.*
    链接

    蓝天旭日 2008-01-16 10:39:00

    学习下!!

  29. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2008-01-16 16:07:00

    为什么要用context.Items["ActionAlreadyWritten"]标记action仅仅改变一次?如果存在多个Form,虽然并不建议这样做,但仍然可能捧到这样的情况,那就会出问题咯。

    另外你为什么要在要发布RSS?

  30. 老赵
    admin
    链接

    老赵 2008-01-16 18:11:00

    @Cat Chen
    理论上是会出现的,最好能够按照Form实例来记录。

  31. 有个问题 [未注册用户]
    *.*.*.*
    链接

    有个问题 [未注册用户] 2008-01-16 21:00:00

    商业组件 ISAPI Rewrite
    规则:RewriteRule /(\w+)/CityTopic/? /CityTopic.aspx?City=$1 [I,U]

    客户端的原始请求URL存在哪?

  32. 老赵
    admin
    链接

    老赵 2008-01-16 21:21:00

    @有个问题
    文章里说了,就放在HTTP_X_REWRITE_URL服务器变量里。

  33. 再次打扰,麻烦了[未注册用户]
    *.*.*.*
    链接

    再次打扰,麻烦了[未注册用户] 2008-01-16 22:30:00

    我在context.Request.ServerVariables["HTTP_X_REWRITE_URL"]时候获取的值依然是类似…/CityTopic.aspx?City=,不知道是不是ISAPI Rewrite和IIRF存值的地方不一样

  34. 老赵
    admin
    链接

    老赵 2008-01-16 23:55:00

    @再次打扰,麻烦了
    查一下文档吧

  35. 一抹微蓝
    *.*.*.*
    链接

    一抹微蓝 2008-01-17 17:44:00

    UrlRewritingNet解决了PostBack地址的问题。

    读老赵的文章受益哦

  36. jeff jing[未注册用户]
    *.*.*.*
    链接

    jeff jing[未注册用户] 2008-02-19 21:50:00

    我按照楼上一位兄弟的写:
    <rewrite url="~/List(.+)\.htm$" to="~/aa.aspx?id=$1" processing="stop" />

    但是却报:配置错误
    分析器错误信息: 未能加载类型“Sample.Web.UI.Adapters.FormRewriterControlAdapter”。

    行 26: <browser refID="Default">
    行 27: <controlAdapters>
    行 28: <adapter controlType="System.Web.UI.HtmlControls.HtmlForm" adapterType="Sample.Web.UI.Adapters.FormRewriterControlAdapter" />
    行 29: </controlAdapters>
    行 30: </browser>

  37. 老赵
    admin
    链接

    老赵 2008-02-19 22:20:00

    @jeff jing
    Sample.Web.UI.Adapters.FormRewriterControlAdapter没找到啊。

  38. Boon Chu[未注册用户]
    *.*.*.*
    链接

    Boon Chu[未注册用户] 2008-02-21 11:16:00

    @再次打扰,麻烦了
    估计你是ServerVariables["HTTP_X_REWRITE_URL"]没有取到值的原因。
    我也曾碰到过类似情况。当时配置文件中是这样的:... [I, L, U]后改成:... [U,I,L] 取值成功。试下,好运!

  39. 老赵
    admin
    链接

    老赵 2008-02-21 11:21:00

    @Boon Chu
    我记得好像是没有区别的.

  40. 无名无姓[未注册用户]
    *.*.*.*
    链接

    无名无姓[未注册用户] 2008-02-27 17:48:00

    赵老师能详细的介绍一下urlwriter.net啊..
    E文没怎么看懂啊..

  41. 榕城小榕
    *.*.*.*
    链接

    榕城小榕 2008-08-05 10:45:00

    System.Web.UI.Adapters.ControlAdapter

    继承这个类,如何实现更改RUNAT=SERVER的HEAD标记里面META的内容,也就是想要动态地添加META标签,应该怎么做,而不是在每一个ASPX或ASPX.CS页面添加代码,



    我的代码如下,不过达不到我想要的效果,望指点

    public class HeadRewriterControlAdapter : System.Web.UI.Adapters.ControlAdapter

    {

    public HeadRewriterControlAdapter()

    {

    //

    // TODO: 在此处添加构造函数逻辑

    //

    }

    protected override void Render(HtmlTextWriter writer)

    {

    base.Render(new HEADRewriteFormHtmlTextWriter(writer));

    }

    }







    public class HEADRewriteFormHtmlTextWriter : System.Web.UI.HtmlTextWriter

    {



    public HEADRewriteFormHtmlTextWriter(HtmlTextWriter writer)

    : base(writer)

    {



    // writer.RenderBeginTag(HtmlTextWriterTag.Head);

    writer.AddAttribute("name", "keywords");

    writer.AddAttribute("content", "mp3 音乐 歌曲 搜索");

    writer.RenderBeginTag(HtmlTextWriterTag.Meta);

    writer.RenderEndTag();

    // writer.RenderEndTag();



    this.InnerWriter = writer.InnerWriter;

    }

    //这样添加的META是显示在HTML标签下面,而不是HEAD下面.如果去掉上面的两行注释,页面上将会添加两个head标签

    public HEADRewriteFormHtmlTextWriter(System.IO.TextWriter writer)

    : base(writer)

    {



    this.InnerWriter = writer;

    }



    }







    浏览器文件内容如下



    <browsers>





    <browser refID="Default">



    <controlAdapters>

    <adapter controlType="System.Web.UI.HtmlControls.HtmlHead"

    adapterType="HeadRewriterControlAdapter" />

    </controlAdapters>

    </browser>

    </browsers>

  42. 水言木
    *.*.*.*
    链接

    水言木 2008-08-06 22:19:00

    楼主的文章就是好!

  43. king2003[未注册用户]
    *.*.*.*
    链接

    king2003[未注册用户] 2008-09-04 19:06:00

    我这个成功不了呀!第一次加载时看FORM的ACTION是对的,可POSTBACK后又变了,是啥原因呀

  44. -brian-
    *.*.*.*
    链接

    -brian- 2009-03-09 14:52:00

    谢谢,学到东西了

  45. 平静中的疯狂
    *.*.*.*
    链接

    平静中的疯狂 2009-07-17 15:09:00

    感谢,完美的解决了我的问题

  46. freewind22[未注册用户]
    *.*.*.*
    链接

    freewind22[未注册用户] 2009-12-04 11:23:00

    我的程序里得不到 HTTP_X_REWRITE_URL 这个值,是空的。

    RewriteRule ^/nmn/member/(.*?)\.html /nmn/member/$1.aspx [I, L, U]

    这里也做了修改。 点提交后就转到aspx页面了。

    里面的FormRewriterControlAdapter也是执行了的。
    我在给value赋值的时候输出了一下
    value = context.Request.ServerVariables["HTTP_X_REWRITE_URL"] ?? context.Request.RawUrl;
    context.Response.Write(value);
    查看源代码就是这样的

    method="post"/nmn/member/register.aspx action="/nmn/member/register.aspx"

    这是怎么回事.

  47. yinzixin
    *.*.*.*
    链接

    yinzixin 2010-03-20 10:10:00

    嗯嗯 挺好的 微软出这个的时候为什么没考虑到这点? 出了个半吊子

  48. 老赵
    admin
    链接

    老赵 2010-03-20 13:49:00

    @yinzixin
    满足了这点还会有其他东西不满足的,不能期待微软做掉任何东西。
    微软的状态就是已经做的过多了,让一部分人觉得没啥好做,一部分觉得还不够。
    但其实你只要多了解一些东西,你会觉得其实两者都是有失偏颇的,我就这么认为。

  49. sky
    113.106.194.*
    链接

    sky 2010-07-16 15:12:33

    赵老师你好,我按照你说的操作了,可是出现如下错误 分析器错误信息: 未能加载类型“Sample.Web.UI.Adapters.FormRewriterControlAdapter”。

    源错误:

    行 25: 行 26: 行 27: 行 28: 行 29:

    是什么原因,该如何解决呢,谢谢

  50. sky
    112.92.40.*
    链接

    sky 2010-07-16 16:37:09

    老赵,谢谢了,问题解决了

  51. 古道轻风
    222.90.16.*
    链接

    古道轻风 2010-09-16 10:55:48

    在使用urlMappings或者urlRewrite.Net之后,form的action我是这么做的,请问是否正确?

    在dotNetFramework2.0,因为在xxx.aspx.cs不支持form.Action="xxx",所以我在页面基类PageBase重写onload.然后将请求的rawURL赋值给form的action: Form.Attributes["action"] = Request.RawUrl; 在dotNetFramework3.5,直接使用Form.Action = Request.RawUrl; 另外,在在dotNetFramework4里面,使用urlMappings之后,form的action会自己处理成重写后的url。 urlMappings不被IIS7支持,不建议使用。

    以上做法是否正确?

  52. 笨鸟
    123.121.235.*
    链接

    笨鸟 2010-10-16 10:30:28

    赵老师你好,Url重写后页面图片使用的是相对路径所有图片都不显示了,有没有什么好的办法解决

  53. lezhan
    183.17.211.*
    链接

    lezhan 2011-11-16 16:35:21

    赵老师您好,我按照你的IIRF方法测试了一下,我的服务器用的是windows2003,可是关于postback的问题并未解决,不知道是什么原因,貌似未执行Form.browser!

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我