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匹配则会进行跳转,否则只是进行请求映射。
<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.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 { get; set; }
public string SendTo { get; set; }
}
}
下面这个类用来获取映射表,当程序第一次运行时会将映射表反序列化的结果放到全局应用程序缓存中。
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.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的基础上稍作修改。
{
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的一部分。