ASP.NET URL双向改写的实现

  我们在进行Web程序开发时,为了进行搜索引擎优化(SEO),往往需要对web的访问地址进行优化,如将http://localhost/Default.aspx?tab=performance修改为http://localhost/Default_performance.aspx,后一个地址能够更好地被搜索引擎搜索到,从而达到了搜索引擎优化的目的。微软有一个开源类库URLRewriter可以非常方便地实现url改写,通过配置在web.config文件中的映射表将用户的请求重定向到具体的页面中,我在“使用URLRewriter进行URL重写失效”一文中详细介绍了如何使用这个类库,该类库是通过asp.net的httpmodules或httphandles来执行的,但如果网站的宿主服务器不支持asp.net httpmodules和httphandles,则该功能便失效了,这时我们可以通过global中的application_beginrequest事件来进行url重定向。本文在URLRewriter类库的基础上进行了改进,并给出了一个相对完整的解决方案。

  我们的改进是建立在URLRewriter的基础之上的,所以URLRewriter原有的东西只要能用,我们都可以直接拿过来,当然,不好的东西要摒弃!

  URLRewriter的映射表是直接写在web.config文件中的,要让web.config能识别映射表,必须在configSections节中添加section,告诉程序如何正确解析web.config中未被识别的内容,如原URLRewriter就需要在web.config中添加<section name="RewriterConfig" type="URLRewriter.Config.RewriterConfigSerializerSectionHandler, URLRewriter"/>。我觉得这个方式并不好,首先你需要单独去编写一个类库来解析xml,并在web.config中进行配置,我们完全可以省去这一步。url的映射表可以单独写到一个xml文件中,当程序运行时将xml加载到应用程序缓存中,并设置一个缓存文件依赖项,这样每当管理员修改完映射表后就可以马上生效。

  另外我希望支持url的双向改写,即上面提到的两个url,当用户输入第二个url时程序会将请求发送到第一个url,但浏览器中显示的url不变;当用户输入第一个url时,自动跳转到第二个url,此时浏览器中显示的是第二个url,但是请求仍然是第一个url。听起来是不是有点绕啊?没关系,其实也很简单,基本的需求就是说客户原来网站中的很多页面在访问时都带了很多参数,做url改写时都换成新的url了,这时旧的url仍然可以用,客户想的就是当输入原来旧的url时能自动跳转到新的url。这个就是url的双向改写!这两种方式可以分别通过Context.RewritePath()和Context.Response.Redirect()方法来实现,下面我们来看具体的实现。

  首先是映射表的实现。我在URLRewriter原有映射表的基础上做了一点改动,就是给ReWriterRule添加了一个IsDirect属性,该属性可选,默认值为False,当值为真时如果用户请求的url匹配则会进行跳转,否则只是进行请求映射。 

<?xml version="1.0"?>
<ReWriterConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  
<Rules>
    
<ReWriterRule>
      
<LookFor>~/Default_(\w+)\.aspx</LookFor>
      
<SendTo>~/Default.aspx?tab=$1</SendTo>
    
</ReWriterRule>
    
<ReWriterRule IsDirect="true">
      
<LookFor>~/Default\.aspx\?tab=(\w+)</LookFor>
      
<SendTo>~/Default_$1.aspx</SendTo>
    
</ReWriterRule>
  
</Rules>
</ReWriterConfig>

  该映射表支持正则表达式,下面是对应的实体类,用来进行反序列化。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace URLRewriterTest
{
    [Serializable]
    
public class ReWriterConfig
    {
        
public ReWriterRule[] Rules;
    }

    [Serializable]
    
public class ReWriterRule
    {
        
private bool _isRedirect = false;

        [System.Xml.Serialization.XmlAttribute(
"IsDirect")]
        
public bool IsRedirect
        {
            
get { return _isRedirect; }
            
set { this._isRedirect = value; }
        }
        
public string LookFor { getset; }
        
public string SendTo { getset; }
    }
}

  下面这个类用来获取映射表,当程序第一次运行时会将映射表反序列化的结果放到全局应用程序缓存中。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Xml.Serialization;
using System.IO;
using System.Web.Caching;

namespace URLRewriterTest
{
    
public class ReWriterConfiguration
    {
        
public static ReWriterConfig GetConfig(string filename)
        {
            
if (HttpContext.Current.Cache["RewriterConfig"== null)
            {
                ReWriterConfig config 
= null;
                
// Create an instance of the XmlSerializer specifying type and namespace.   
                XmlSerializer serializer = new XmlSerializer(typeof(ReWriterConfig));

                
// A FileStream is needed to read the XML document.   
                using (Stream reader = new FileStream(filename, FileMode.Open))
                {
                    
// Declare an object variable of the type to be deserialized.   
                    config = (ReWriterConfig)serializer.Deserialize(reader);
                }
                HttpContext.Current.Cache.Insert(
"RewriterConfig", config, new CacheDependency(filename));
            }
            
return (ReWriterConfig)HttpContext.Current.Cache["RewriterConfig"];     
        }
    }
}

  我们仍然需要原URLRewriter类库中的ReWriterUtils类中的方法,不过对其中RewriteUrl方法进行了一点小的改动,增加了一个isRedirect参数,用来决定是执行Context.RewritePath()方法还是Context.Response.Redirect()方法,下面是源代码。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace URLRewriterTest
{
    
public class ReWriterUtils
    {
        
/// <summary>
        
/// Rewrite's a URL using <b>HttpContext.RewriteUrl()</b>.
        
/// </summary>
        
/// <param name="context">The HttpContext object to rewrite the URL to.</param>
        
/// <param name="isRedirect">Redirect or rewrite path.</param>
        
/// <param name="sendToUrl">The URL to rewrite to.</param>
        public static void RewriteUrl(HttpContext context, string sendToUrl, bool isRedirect)
        {
            
string x, y;
            RewriteUrl(context, sendToUrl, isRedirect, 
out x, out y);
        }

        
/// <summary>
        
/// Rewrite's a URL using <b>HttpContext.RewriteUrl()</b>.
        
/// </summary>
        
/// <param name="context">The HttpContext object to rewrite the URL to.</param>
        
/// <param name="sendToUrl">The URL to rewrite to.</param>
        
/// <param name="isRedirect">Redirect or rewrite path.</param>
        
/// <param name="sendToUrlLessQString">Returns the value of sendToUrl stripped of the querystring.</param>
        
/// <param name="filePath">Returns the physical file path to the requested page.</param>
        public static void RewriteUrl(HttpContext context, string sendToUrl, bool isRedirect, out string sendToUrlLessQString, out string filePath)
        {
            
// see if we need to add any extra querystring information
            if (context.Request.QueryString.Count > 0)
            {
                
if (sendToUrl.IndexOf('?'!= -1)
                    sendToUrl 
+= "&" + context.Request.QueryString.ToString();
                
else
                    sendToUrl 
+= "?" + context.Request.QueryString.ToString();
            }

            
// first strip the querystring, if any
            string queryString = String.Empty;
            sendToUrlLessQString 
= sendToUrl;
            
if (sendToUrl.IndexOf('?'> 0)
            {
                sendToUrlLessQString 
= sendToUrl.Substring(0, sendToUrl.IndexOf('?'));
                queryString 
= sendToUrl.Substring(sendToUrl.IndexOf('?'+ 1);
            }

            
// grab the file's physical path
            filePath = string.Empty;
            filePath 
= context.Server.MapPath(sendToUrlLessQString);

            
if (isRedirect)
            {
                
// redirect the path
                context.Response.Redirect("~/" + sendToUrlLessQString);
            }
            
else
            {
                
// rewrite the path
                context.RewritePath("~/" + sendToUrlLessQString, String.Empty, queryString);
            }

            
// NOTE!  The above RewritePath() overload is only supported in the .NET Framework 1.1
            
// If you are using .NET Framework 1.0, use the below form instead:
            
// context.RewritePath(sendToUrl);
        }

        
/// <summary>
        
/// Converts a URL into one that is usable on the requesting client.
        
/// </summary>
        
/// <remarks>Converts ~ to the requesting application path.  Mimics the behavior of the 
        
/// <b>Control.ResolveUrl()</b> method, which is often used by control developers.</remarks>
        
/// <param name="appPath">The application path.</param>
        
/// <param name="url">The URL, which might contain ~.</param>
        
/// <returns>A resolved URL.  If the input parameter <b>url</b> contains ~, it is replaced with the
        
/// value of the <b>appPath</b> parameter.</returns>
        public static string ResolveUrl(string appPath, string url)
        {
            
if (url.Length == 0 || url[0!= '~')
                
return url;        // there is no ~ in the first character position, just return the url
            else
            {
                
if (url.Length == 1)
                    
return appPath;  // there is just the ~ in the URL, return the appPath
                if (url[1== '/' || url[1== '\\')
                {
                    
// url looks like ~/ or ~\
                    if (appPath.Length > 1)
                        
return appPath + "/" + url.Substring(2);
                    
else
                        
return "/" + url.Substring(2);
                }
                
else
                {
                    
// url looks like ~something
                    if (appPath.Length > 1)
                        
return appPath + "/" + url.Substring(1);
                    
else
                        
return appPath + url.Substring(1);
                }
            }
        }
    }
}

  最后就是编写Global中的Application_BeginRequest事件了,在原有URLRewriter的基础上稍作修改。

protected void Application_BeginRequest(object sender, EventArgs e)
{
    
string requestedPath = Request.RawUrl.ToString();

    
// get the configuration rules
    string filename = Context.Server.MapPath("."+ "//ReWriterRules.xml";
    ReWriterConfig rules 
= ReWriterConfiguration.GetConfig(filename);

    
// iterate through each rule
    for (int i = 0; i < rules.Rules.Length; i++)
    {
        
// get the pattern to look for, and Resolve the Url (convert ~ into the appropriate directory)
        string lookFor = "^" + ReWriterUtils.ResolveUrl(Context.Request.ApplicationPath, rules.Rules[i].LookFor) + "$";

        
// Create a regex (note that IgnoreCase is set)
        Regex re = new Regex(lookFor, RegexOptions.IgnoreCase);

        
// See if a match is found
        if (re.IsMatch(requestedPath))
        {
            
// match found - do any replacement needed
            string sendToUrl = ReWriterUtils.ResolveUrl(Context.Request.ApplicationPath, re.Replace(requestedPath, rules.Rules[i].SendTo));

            
// Rewrite or redirect the URL
            ReWriterUtils.RewriteUrl(Context, sendToUrl, rules.Rules[i].IsRedirect);
            
break;        // exit the for loop
        }
    }
}

  好了,大功告成!使用上面的映射表,当你输入http://localhost/Default_performance.aspx时访问正常,事实上Default_后面可以添加任何字符,这些字符都将作为Default.aspx页面tab参数的值。同时,当你输入http://localhost/Default.aspx?tab=performance时页面会自动跳转到前面一个url,tab参数的值将被作为url的一部分。

URLRewriterTest.rar下载

标签: URLRewriter
posted @ 2009-10-21 12:14 Jaxu 阅读(2744) 评论(12) 编辑 收藏

 回复 引用   
#1楼2009-10-21 12:57 | bbbK[未注册用户]
多个参数呢
 回复 引用 查看   
#2楼[楼主]2009-10-21 13:14 | Jaxu      
引用bbbK:多个参数呢

多个参数同样也可以啊,你只需要将映射表写好就可以了。
<ReWriterRule>
<LookFor>~/Default_(\w+)_(\w+)\.aspx</LookFor>
<SendTo>~/Default.aspx?tab=$1&id=$2</SendTo>
</ReWriterRule>

 回复 引用   
#3楼2009-10-21 14:52 | hxj[未注册用户]
不明白 为什么不支持httpmodules,httphandles?
都不支持还能运行asp.net么?

 回复 引用 查看   
#4楼2009-10-21 16:02 | JacksonLin      
有人写过这个,我记录过。
怎样也支持下,不要全部依赖MS的功能。
但win7,win08的确已经对这个比较完善。

 回复 引用 查看   
#5楼[楼主]2009-10-21 16:26 | Jaxu      
引用hxj:
不明白 为什么不支持httpmodules,httphandles?
都不支持还能运行asp.net么?

我在http://www.cnblogs.com/jaxu/archive/2009/10/14/1583062.html一篇文章中提到过这种情况,估计是跟服务器的IIS设置有关系,asp.net的httpModules和httpHandles都不起作用,我也非常奇怪,不管怎么配置就是不行。

 回复 引用 查看   
#6楼[楼主]2009-10-21 16:29 | Jaxu      
引用JacksonLin:
有人写过这个,我记录过。
怎样也支持下,不要全部依赖MS的功能。
但win7,win08的确已经对这个比较完善。

iis 7的确对url改写支持得很好,在iis 7上完全没有必要花时间去做这些工作:)

 回复 引用 查看   
#7楼2009-10-21 18:27 | 莫耶      
用mvc吧
 回复 引用   
#8楼2009-12-03 11:09 | chpsp[未注册用户]
URLRewriterTest.rar下载
怎么下载不了啦??

 回复 引用   
#9楼2009-12-03 11:13 | chpsp[未注册用户]
可以把那个程序代码发给我么???
 回复 引用 查看   
#10楼[楼主]2009-12-03 13:03 | Jaxu      
引用chpsp:
URLRewriterTest.rar下载
怎么下载不了啦??

引用chpsp:可以把那个程序代码发给我么???

我试了可以下载啊?你换个地方再试试看。

 回复 引用 查看   
#11楼2010-05-03 12:59 | Jack Sun      
  if (isRedirect)
            {
                // redirect the path
                context.Response.Redirect("~/" + sendToUrlLessQString);
            }



有个以为 当设置isRedirect=true时,跳转的页面也应该是带参数的呀?sendToUrlLessQString这个好像已经把参数去了....

 回复 引用 查看   
#12楼[楼主]2010-05-04 09:11 | Jaxu      
引用Jack Sun:
  if (isRedirect)
            {
                // redirect the path
                context.Response.Redirect("~/" + sendToUrlLessQString);
            }



有个以为 当设置isRedirect=true时,跳转的页面也应该是带参数的呀?sendToUrlLessQString这个好像已经把参数去了....

如果是设置了页面的URL跳转,那为什么还需要参数呢?如果需要参数的话那就只是URL改写了,和跳转不是一回事。例如请求的地址是http://www.mysite.com/test.aspx?id=1,跳转的地址可能就是http://www.mysite.com/1/default.aspx,这个时候根本就不要参数了。
设置URL跳转的根本目的就是要去掉URL中难记的查询字符串,让搜索引擎和用户能够清晰地记住所要访问的页面的地址。