老赵点滴


  先做人,再做技术人员,最后做程序员。
  我的理想:“让外国人看中国人写的技术书籍和文章”。Try as I might
posts - 287, comments - 10551, trackbacks - 137, articles - 6
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理

  在进行了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的一些细节与特点

Feedback

#1楼    回复  引用  查看    

2008-01-13 08:03 by 韩现龙      
好人啊老赵!
虽然还没仔细看这篇文章,可是已经知道它就是我昨天在思考问题的解决方案了。

#2楼    回复  引用  查看    

2008-01-13 10:19 by 丁学      
呵呵,Control Adaptor是个好东西啊

#3楼    回复  引用  查看    

2008-01-13 10:42 by SZW      
学习学习
这个应该可以很好地解决DiscuzNT这方面的问题

#4楼    回复  引用  查看    

2008-01-13 10:49 by 海风吹呀吹      
老赵的文章就是不一样!哈哈!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 by 海风吹呀吹      
需要美刀不可怕,呵呵,偶已经买了一个

#6楼    回复  引用  查看    

2008-01-13 11:33 by Goumh      
好文章,学习.......

#7楼    回复  引用    

2008-01-13 11:57 by Zero One [未注册用户]
思路清晰,介绍系统,好文章

#8楼    回复  引用    

2008-01-13 12:34 by 5yplan [未注册用户]
精力充沛呀~算的上是一日三篇了。呵呵

#9楼 [楼主]   回复  引用  查看    

2008-01-13 12:41 by Jeffrey Zhao      
@5yplan
平时没时间写,有机会多写几篇,呵呵。

#10楼    回复  引用  查看    

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

#11楼    回复  引用  查看    

2008-01-13 14:05 by 江大鱼      
叶面load完成之后再把url再rewrite回去也是一个不错的方法,

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

#12楼 [楼主]   回复  引用  查看    

2008-01-13 14:07 by Jeffrey Zhao      
@SZW
肯定可以阿,我们可以把前者Rewrite到后者,自然可以反过来,呵呵。

#13楼 [楼主]   回复  引用  查看    

2008-01-13 14:08 by Jeffrey Zhao      
@江大鱼
你的页面被提示有木马……

#14楼    回复  引用  查看    

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

#15楼 [楼主]   回复  引用  查看    

2008-01-13 15:33 by Jeffrey Zhao      
@SZW
我没有理解你的意思,什么东西会简化很多?

#16楼    回复  引用  查看    

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

#17楼    回复  引用  查看    

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

#18楼    回复  引用  查看    

2008-01-13 16:06 by Argo      
这个Intelligencia.UrlRewriter内就已经有对ControlAdapter的支持了,只需要如下配置Browser文件即可。
<browsers>
  <browser refID="Default">
    <controlAdapters>
      <adapter controlType="System.Web.UI.HtmlControls.HtmlForm" adapterType="Intelligencia.UrlRewriter.FormRewriterControlAdapter" />
    </controlAdapters>
  </browser>
</browsers>

#19楼 [楼主]   回复  引用  查看    

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

#20楼 [楼主]   回复  引用  查看    

2008-01-13 16:27 by Jeffrey Zhao      
@Argo
多谢补充。:)

#21楼    回复  引用  查看    

2008-01-13 16:42 by 韩现龙      
刚一不小心发现一个问题!!
情景如下:
这是我的重写规则之一:


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

#22楼    回复  引用  查看    

2008-01-13 16:43 by 韩现龙      
url="~/list(.+).htm" to="~/frmNewsList.aspx?id=$1" processing="stop"

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

#23楼 [楼主]   回复  引用  查看    

2008-01-13 16:56 by Jeffrey Zhao      
@韩现龙
url="^/List(.+)\.htm$"

#24楼    回复  引用  查看    

2008-01-13 17:00 by 韩现龙      
@Jeffrey Zhao
。。竟然忘了是正则的事

#25楼    回复  引用  查看    

2008-01-13 18:47 by airwolf2026      
学习了.以前一直纳闷的类似/user/Contact.do可以打开一个html页面的迷惑总算解开了.

#26楼    回复  引用  查看    

2008-01-13 21:12 by MK2      
呵呵,感谢老赵的好文,在UrlRewriter.NET中也发现了该作者也对Form的Action问题改写了Form类。

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

#27楼    回复  引用  查看    

2008-01-14 16:42 by 一路走好      
學習

#28楼    回复  引用  查看    

2008-01-16 10:39 by 蓝天旭日      
学习下!!

#29楼    回复  引用  查看    

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

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

#30楼 [楼主]   回复  引用  查看    

2008-01-16 18:11 by Jeffrey Zhao      
@Cat Chen
理论上是会出现的,最好能够按照Form实例来记录。

#31楼    回复  引用    

2008-01-16 21:00 by 有个问题 [未注册用户]
商业组件 ISAPI Rewrite
规则:RewriteRule /(\w+)/CityTopic/? /CityTopic.aspx?City=$1 [I,U]

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

#32楼 [楼主]   回复  引用  查看    

2008-01-16 21:21 by Jeffrey Zhao      
@有个问题
文章里说了,就放在HTTP_X_REWRITE_URL服务器变量里。

#33楼    回复  引用    

2008-01-16 22:30 by 再次打扰,麻烦了 [未注册用户]
我在context.Request.ServerVariables["HTTP_X_REWRITE_URL"]时候获取的值依然是类似…/CityTopic.aspx?City=,不知道是不是ISAPI Rewrite和IIRF存值的地方不一样

#34楼 [楼主]   回复  引用  查看    

2008-01-16 23:55 by Jeffrey Zhao      
@再次打扰,麻烦了
查一下文档吧

#35楼    回复  引用  查看    

2008-01-17 17:44 by 一抹微蓝      
UrlRewritingNet解决了PostBack地址的问题。

读老赵的文章受益哦

#36楼    回复  引用    

2008-02-19 21:50 by jeff jing [未注册用户]
我按照楼上一位兄弟的写:
<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楼 [楼主]   回复  引用  查看    

2008-02-19 22:20 by Jeffrey Zhao      
@jeff jing
Sample.Web.UI.Adapters.FormRewriterControlAdapter没找到啊。

#38楼    回复  引用    

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

#39楼 [楼主]   回复  引用  查看    

2008-02-21 11:21 by Jeffrey Zhao      
@Boon Chu
我记得好像是没有区别的.

#40楼    回复  引用    

2008-02-27 17:48 by 无名无姓 [未注册用户]
赵老师能详细的介绍一下urlwriter.net啊..
E文没怎么看懂啊..

标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2008-01-13 16:36 编辑过
 
历史上的今天:

另存  打印