Spiga

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

2008-01-13 03:17 by Jeffrey Zhao, 17564 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的一些细节与特点

Add your comment

48 条回复

  1. #1楼 韩现龙      2008-01-13 08:03
    好人啊老赵!
    虽然还没仔细看这篇文章,可是已经知道它就是我昨天在思考问题的解决方案了。
     回复 引用 查看   
  2. #2楼 丁学      2008-01-13 10:19
    呵呵,Control Adaptor是个好东西啊
     回复 引用 查看   
  3. #3楼 SZW      2008-01-13 10:42
    学习学习
    这个应该可以很好地解决DiscuzNT这方面的问题
     回复 引用 查看   
  4. #4楼 海风吹呀吹      2008-01-13 10:49
    老赵的文章就是不一样!哈哈!URL Rewrite实现URL重写的确是实现静态的一个理想解决方案!apache不用多讨论了,相信大家都知道了它的强大,但是在IIS下面,实现apache的功能,恐怕非ISAPI_Rewrite莫属(虽然不是微软的东西),基于正则的ISAPI_Rewrite的优先解析比.NET自带的那个理论上应该要快吧?如果老赵对ISAPI_Rewrite熟悉,强烈要求老赵为新手们写一下关于ISAPI_Rewrite有关实现无限二级域名的文章,特别是ISAPI_Rewrite如何让二级域名保持不变,这个问题一直是困扰新手们很久的问题。(比方说http://JeffreyZhao.cnblogs.com/点了回车之后URL地址不要变)
     回复 引用 查看   
  5. #5楼 海风吹呀吹      2008-01-13 11:28
    需要美刀不可怕,呵呵,偶已经买了一个
     回复 引用 查看   
  6. #6楼 Goumh      2008-01-13 11:33
    好文章,学习.......
     回复 引用 查看   
  7. #7楼 Zero One[未注册用户]2008-01-13 11:57
    思路清晰,介绍系统,好文章
     回复 引用   
  8. #8楼 5yplan[未注册用户]2008-01-13 12:34
    精力充沛呀~算的上是一日三篇了。呵呵
     回复 引用   
  9. #9楼[楼主] Jeffrey Zhao      2008-01-13 12:41
    @5yplan
    平时没时间写,有机会多写几篇,呵呵。
     回复 引用 查看   
  10. #10楼 SZW      2008-01-13 14:05
    @Jeffrey Zhao
    有没有办法可以同样利用正则表达式,获取~/User.aspx?name=$1翻译成~/User/[name](这个应该不是问题),然后在页面生成的时候,加到form/server的action中呢?可以的话这样就可以在ASP.NET内部把这个问题处理掉了。
     回复 引用 查看   
  11. #11楼 江大鱼      2008-01-13 14:05
    叶面load完成之后再把url再rewrite回去也是一个不错的方法,

    http://www.cnblogs.com/jzywh/archive/2007/12/20/urlrewriteaction.html
     回复 引用 查看   
  12. #12楼[楼主] Jeffrey Zhao      2008-01-13 14:07
    @SZW
    肯定可以阿,我们可以把前者Rewrite到后者,自然可以反过来,呵呵。
     回复 引用 查看   
  13. #13楼[楼主] Jeffrey Zhao      2008-01-13 14:08
    @江大鱼
    你的页面被提示有木马……
     回复 引用 查看   
  14. #14楼 SZW      2008-01-13 14:14
    @Jeffrey Zhao
    那么做不是可以把问题简化很多吗?江大鱼提供的例子似乎有点符合这个意思,不过我还没测试。
     回复 引用 查看   
  15. #15楼[楼主] Jeffrey Zhao      2008-01-13 15:33
    @SZW
    我没有理解你的意思,什么东西会简化很多?
     回复 引用 查看   
  16. #16楼 Leem      2008-01-13 15:44
    --引用--------------------------------------------------
    SZW: @Jeffrey Zhao
    那么做不是可以把问题简化很多吗?江大鱼提供的例子似乎有点符合这个意思,不过我还没测试。
    --------------------------------------------------------
    Adaptor这么优雅的做法你不用,为什么一定要自己在页面里处理呢.
     回复 引用 查看   
  17. #17楼 SZW      2008-01-13 15:56
    @Leem
    Adaptor是很好,老赵的方法我也正在测试用到一些地方,我只是提出一种另外一种做法的可能,另外按照江大鱼给的例子来看,也不是每个页面都需要一对一地处理,它是基于basepage的,所以“优雅”一点上来说,页面和不页面已经不是重点。
     回复 引用 查看   
  18. #18楼 Argo      2008-01-13 16:06
    这个Intelligencia.UrlRewriter内就已经有对ControlAdapter的支持了,只需要如下配置Browser文件即可。
    <browsers>
      <browser refID="Default">
        <controlAdapters>
          <adapter controlType="System.Web.UI.HtmlControls.HtmlForm" adapterType="Intelligencia.UrlRewriter.FormRewriterControlAdapter" />
        </controlAdapters>
      </browser>
    </browsers>
     回复 引用 查看   
  19. #19楼[楼主] Jeffrey Zhao      2008-01-13 16:27
    @SZW
    嗯,如果写在BasePage里可能也不错吧。不过这个方法相对于Adaptor来说侵入性还是高了点,如果一个页面又没有Form那么怎么办呢?用Adaptor的话,相当于将这部分逻辑提取出来了,而且是面向Form本身,而不是页面。
    江大鱼的例子可能也会造成问题,OnLoadComplete后面还有很多生命周期的过程呢,万一某个地方有依赖的话……所以相比起来我还是觉得Adaptor更好。
     回复 引用 查看   
  20. #20楼[楼主] Jeffrey Zhao      2008-01-13 16:27
    @Argo
    多谢补充。:)
     回复 引用 查看   
  21. #21楼 韩现龙      2008-01-13 16:42
    刚一不小心发现一个问题!!
    情景如下:
    这是我的重写规则之一:


    当我输入"list1.htm"时页面能够正常显示,可如果输入"list1.htm/"时页面就无法正常显示了~~样式什么的都没了,而且在IE7下提示"Stack overflow at line:0 ",在FF下是“正在从localhost加载数据”和“等待...."轮换显示。
    老赵,这是怎么回事啊?
     回复 引用 查看   
  22. #22楼 韩现龙      2008-01-13 16:43
    url="~/list(.+).htm" to="~/frmNewsList.aspx?id=$1" processing="stop"

    上面是我的重写规则。
    晕,尖括号没显示出来。
     回复 引用 查看   
  23. #23楼[楼主] Jeffrey Zhao      2008-01-13 16:56
    @韩现龙
    url="^/List(.+)\.htm$"
     回复 引用 查看   
  24. #24楼 韩现龙      2008-01-13 17:00
    @Jeffrey Zhao
    。。竟然忘了是正则的事
     回复 引用 查看   
  25. #25楼 airwolf2026      2008-01-13 18:47
    学习了.以前一直纳闷的类似/user/Contact.do可以打开一个html页面的迷惑总算解开了.
     回复 引用 查看   
  26. #26楼 MK2      2008-01-13 21:12
    呵呵,感谢老赵的好文,在UrlRewriter.NET中也发现了该作者也对Form的Action问题改写了Form类。

    呵呵,继续收藏剩下的一篇。
     回复 引用 查看   
  27. #27楼 一路走好      2008-01-14 16:42
    學習
     回复 引用 查看   
  28. #28楼 蓝天旭日      2008-01-16 10:39
    学习下!!
     回复 引用 查看   
  29. #29楼 Cat Chen      2008-01-16 16:07
    为什么要用context.Items["ActionAlreadyWritten"]标记action仅仅改变一次?如果存在多个Form,虽然并不建议这样做,但仍然可能捧到这样的情况,那就会出问题咯。

    另外你为什么要在要发布RSS?
     回复 引用 查看   
  30. #30楼[楼主] Jeffrey Zhao      2008-01-16 18:11
    @Cat Chen
    理论上是会出现的,最好能够按照Form实例来记录。
     回复 引用 查看   
  31. #31楼 有个问题 [未注册用户]2008-01-16 21:00
    商业组件 ISAPI Rewrite
    规则:RewriteRule /(\w+)/CityTopic/? /CityTopic.aspx?City=$1 [I,U]

    客户端的原始请求URL存在哪?
     回复 引用   
  32. #32楼[楼主] Jeffrey Zhao      2008-01-16 21:21
    @有个问题
    文章里说了,就放在HTTP_X_REWRITE_URL服务器变量里。
     回复 引用 查看   
  33. #33楼 再次打扰,麻烦了[未注册用户]2008-01-16 22:30
    我在context.Request.ServerVariables["HTTP_X_REWRITE_URL"]时候获取的值依然是类似…/CityTopic.aspx?City=,不知道是不是ISAPI Rewrite和IIRF存值的地方不一样
     回复 引用   
  34. #34楼[楼主] Jeffrey Zhao      2008-01-16 23:55
    @再次打扰,麻烦了
    查一下文档吧
     回复 引用 查看   
  35. #35楼 一抹微蓝      2008-01-17 17:44
    UrlRewritingNet解决了PostBack地址的问题。

    读老赵的文章受益哦
     回复 引用 查看   
  36. #36楼 jeff jing[未注册用户]2008-02-19 21:50
    我按照楼上一位兄弟的写:
    <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. #37楼[楼主] Jeffrey Zhao      2008-02-19 22:20
    @jeff jing
    Sample.Web.UI.Adapters.FormRewriterControlAdapter没找到啊。
     回复 引用 查看   
  38. #38楼 Boon Chu[未注册用户]2008-02-21 11:16
    @再次打扰,麻烦了
    估计你是ServerVariables["HTTP_X_REWRITE_URL"]没有取到值的原因。
    我也曾碰到过类似情况。当时配置文件中是这样的:... [I, L, U]后改成:... [U,I,L] 取值成功。试下,好运!
     回复 引用   
  39. #39楼[楼主] Jeffrey Zhao      2008-02-21 11:21
    @Boon Chu
    我记得好像是没有区别的.
     回复 引用 查看   
  40. #40楼 无名无姓[未注册用户]2008-02-27 17:48
    赵老师能详细的介绍一下urlwriter.net啊..
    E文没怎么看懂啊..
     回复 引用   
  41. #41楼 榕城小榕      2008-08-05 10:45
    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. #42楼 水言木      2008-08-06 22:19
    楼主的文章就是好!
     回复 引用 查看   
  43. #43楼 king2003[未注册用户]2008-09-04 19:06
    我这个成功不了呀!第一次加载时看FORM的ACTION是对的,可POSTBACK后又变了,是啥原因呀
     回复 引用   
  44. #44楼 -brian-      2009-03-09 14:52
    谢谢,学到东西了
     回复 引用 查看   
  45. #45楼 平静中的疯狂      2009-07-17 15:09
    感谢,完美的解决了我的问题
     回复 引用 查看   
  46. #46楼 freewind22[未注册用户]2009-12-04 11:23
    我的程序里得不到 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. #47楼 yinzixin      2010-03-20 10:10
    嗯嗯 挺好的 微软出这个的时候为什么没考虑到这点? 出了个半吊子
     回复 引用 查看   
  48. #48楼[楼主] Jeffrey Zhao      2010-03-20 13:49
    @yinzixin
    满足了这点还会有其他东西不满足的,不能期待微软做掉任何东西。
    微软的状态就是已经做的过多了,让一部分人觉得没啥好做,一部分觉得还不够。
    但其实你只要多了解一些东西,你会觉得其实两者都是有失偏颇的,我就这么认为。
     回复 引用 查看   
发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 1036688 G6Rnie7kMAU=