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中添加。我覺得這個方式并不好,首先你需要單獨去編寫一個類庫來解析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+)\.aspxLookFor>
<SendTo>~/Default.aspx?tab=$1SendTo>
ReWriterRule>
<ReWriterRule IsDirect="true">
<LookFor>~/Default\.aspx\?tab=(\w+)LookFor>
<SendTo>~/Default_$1.aspxSendTo>
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
{
///
/// Rewrite's a URL using HttpContext.RewriteUrl().
///
/// The HttpContext object to rewrite the URL to.
/// Redirect or rewrite path.
/// The URL to rewrite to.
public static void RewriteUrl(HttpContext context, string sendToUrl,
bool isRedirect)
{
string x, y;
RewriteUrl(context, sendToUrl, isRedirect, out x, out y);
}
///
/// Rewrite's a URL using HttpContext.RewriteUrl().
///
/// The HttpContext object to rewrite the URL to.
/// The URL to rewrite to.
/// Redirect or rewrite path.
/// Returns the value of sendToUrl stripped of the querystring.
/// Returns the physical file path to the requested page.
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);
}
///
/// Converts a URL into one that is usable on the requesting client.
///
/// Converts ~ to the requesting application path. Mimics the behavior of the
/// Control.ResolveUrl() method, which is often used by control developers.
/// The application path.
/// The URL, which might contain ~.
/// A resolved URL. If the input parameter url contains ~, it is replaced with the
/// value of the appPath parameter.
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的一部分。