ASP.NET的地址重寫(URLRewriter)實現原理及代碼示例
一、概述
訪問者輸入:http://wu-jian.cnbolgs.com/default.aspx,實際請求和響應的地址卻是:http://www.cnblogs.com/wu-jian/default.aspx, 這就是UrlRewrite,除了實現二級域名功能,它在簡化用戶輸入地址、SEO、網站版本迭代更新等多個方面發揮著重要作用。
微軟曾在.net framework 1.1中提供過一個名為URLRewriter的小工具供開發人員輕松實現UrlRewrite,下載地址為:http://download.microsoft.com/download/0/4/6/0463611e-a3f9-490d-a08c-877a83b797cf/MSDNURLRewriting.msi
本文以URLRewriter為例,在.net framework 2.0的環境下做了小部分優化調整,供大家學習和參考,能力有限,不足之處請大家及時指出。本文假設讀者對URLRewriter、ASP.net的 Http管線有一定了解,否則請查閱相關資料。
二、配置
URLRewriter在web.config里通過自定義配置結合正則表達式來實現URL重寫。
自定義節點的聲明:
<section name="RewriterConfig"
type="PaoTiao.PTRewriter.Config.RewriterConfigSerializerSectionHandler, PaoTiao.PTRewriter" />
</configSections>
自定義節點配置項:
<Rules>
<RewriterRule>
<LookFor>^http://([a-zA-Z0-9]{4,16}).cnblogs.com/default.aspx$</LookFor>
<SendTo>/$1/default.aspx</SendTo>
</RewriterRule>
<RewriterRule>
<LookFor>^http://www.cnblogs.com/([a-zA-Z0-9]{4,16})/$</LookFor>
<SendTo>/test/url.aspx?p=$1</SendTo>
</RewriterRule>
</Rules>
</RewriterConfig>
如上我配置了兩個規則,以實例說明,第一個可將:http://wu-jian.cnblogs.com 重寫到:/wu-jian/default.aspx
第二個可將:http://www.cnblogs.com/wu-jian 重寫到:/test/url.aspx?p=wu-jian
但微軟的URLRewriter LookFor并不支持到域名位置,它只能在根目錄之后做文章,截選了它源碼DEMO中的一段:
<LookFor>~/(\d{4})/(\d{2})/(\d{2})\.aspx</LookFor>
<SendTo>~/ShowBlogContent.aspx?year=$1&month=$2&day=$3</SendTo>
</RewriterRule>
可以發現,當需要使用二級域名或自定義級別更高的rewrite時,URLRewriter是不支持的,所以在此我將源代碼作了一小部分優化,匹配與重寫都使用LookFor和SendTo中的原始表達式,不做任何智能替換與修改。其實很多時候,在微軟的產品中都能發現這種“畫蛇添足”的影子。
關于匹配與替換, 其實就是應用了正則表達式中的“反向引用”原理,在我的博客里有代碼示例,不熟悉正則的朋友可去了解,此處不作詳敘。
三、源代碼分析
對自定義配置進行訪問的類:
using System.Configuration;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.XPath;
namespace PaoTiao.PTRewriter.Config
{
/// <summary>
/// 實現IConfigurationSectionHandler接口,以對自定義節點進行訪問
/// </summary>
public class RewriterConfigSerializerSectionHandler : IConfigurationSectionHandler
{
/// <summary>
/// 該方法無需主動調用
/// 它在ConfigurationManager.GetSection()被調用時根據改配置節聲明中所定義的類名和路徑自動實例化配置節處理類
/// </summary>
public object Create(object parent, object configContext, System.Xml.XmlNode section)
{
XmlSerializer ser = new XmlSerializer(typeof(RewriterConfiguration));
return ser.Deserialize(new XmlNodeReader(section));
}
}//end class
}
之前一直寫WEB程序,很少用到自定義節點,直到一次寫Windows Service用到了app.config,發現要讀取自定義節點,就需要實現IConfigurationSectionHandler接口。
using System.Web;
using System.Web.Caching;
using System.Configuration;
using System.Xml.Serialization;
namespace PaoTiao.PTRewriter.Config
{
[Serializable()]
[XmlRoot("RewriterConfig")]
public class RewriterConfiguration
{
private RewriterRuleCollection rules;
/// <summary>
/// 該方法從web.config中讀取規則集合,并使用了Cache以避免頻繁IO操作
/// </summary>
/// <returns></returns>
public static RewriterConfiguration GetConfig()
{
//使用緩存
if (HttpContext.Current.Cache["RewriterConfig"] == null)
HttpContext.Current.Cache.Insert("RewriterConfig", ConfigurationManager.GetSection("RewriterConfig"));
return (RewriterConfiguration)HttpContext.Current.Cache["RewriterConfig"];
}
public RewriterRuleCollection Rules
{
get { return rules; }
set { rules = value; }
}
}//end class
}
我想使用UrlRewrite的站點絕大部分都是面向公眾用戶的,面向公眾用戶就面臨著大的流量和并發,誰也不愿意為每個請求去讀取一次web.config吧,那么在此處使用Cache是明智之舉。另外更換了已過期的ConfigurationSettings.GetConfig()方法為ConfigurationManager.GetSection()方法。
如下兩個類完成類似的Model功能。
using System.Collections;
namespace PaoTiao.PTRewriter.Config
{
/// <summary>
/// 規則集合
/// </summary>
[Serializable()]
public class RewriterRuleCollection : CollectionBase
{
/// <summary>
/// 向集合中添加新規則
/// </summary>
/// <param name="r">RewriterRule對象</param>
public virtual void Add(RewriterRule r)
{
this.InnerList.Add(r);
}
/// <summary>
/// 獲取或設置項
/// </summary>
public RewriterRule this[int index]
{
get { return (RewriterRule)this.InnerList[index]; }
set { this.InnerList[index] = value; }
}
}//end class
}
namespace PaoTiao.PTRewriter.Config
{
/// <summary>
/// 重寫規則的數據對象
/// </summary>
[Serializable()]
public class RewriterRule
{
private string mLookFor;
private string mSendTo;
/// <summary>
/// 查找規則
/// </summary>
public string LookFor{
get { return this.mLookFor; }
set { this.mLookFor = value; }
}
/// <summary>
/// 重寫規則
/// </summary>
public string SendTo{
get { return this.mSendTo; }
set { this.mSendTo = value; }
}
}//end class
}//end namespace
使用HttpModule實現地址重寫:
using System.Web;
namespace PaoTiao.PTRewriter
{
/// <summary>
/// 實現IHttpModule的抽象類
/// </summary>
public abstract class BaseModuleRewriter : IHttpModule
{
public virtual void Dispose() { }
public virtual void Init(HttpApplication app)
{
app.BeginRequest += new EventHandler(this.BaseModuleRewriter_BeginRequest);
}
protected virtual void BaseModuleRewriter_BeginRequest(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
Rewrite(app);
}
/// <summary>
/// 地址重寫抽象函數
/// </summary>
/// <param name="app"></param>
protected abstract void Rewrite(HttpApplication app);
}//end class
}
在Http模塊中進行核心邏輯處理,源代碼是在AuthorizeRequest事件中,此處我使用了BeginRequest事件。
對抽象方法Rewrite的實現。大家可以發現URL重寫其實就一個核心方法:HttpContext.RewritePath
看看MSDN中對該方法的描述:指定內部重寫路徑,并允許請求的 URL 與資源的內部路徑不同。
using System.Text.RegularExpressions;
using System.Configuration;
using System.IO;
namespace PaoTiao.PTRewriter
{
public class ModuleRewriter : BaseModuleRewriter
{
/// <summary>
/// 地址重寫函數
/// </summary>
/// <param name="app"></param>
protected override void Rewrite(System.Web.HttpApplication app)
{
//開始跟蹤日志
app.Context.Trace.Write("ModuleRewriter", "Entering ModuleRewriter");
//獲取規則集合
Config.RewriterRuleCollection rules = Config.RewriterConfiguration.GetConfig().Rules;
for (int i = 0; i < rules.Count; i++)
{
string lookFor = rules[i].LookFor;
Regex reg = new Regex(lookFor, RegexOptions.IgnoreCase);
if (reg.IsMatch(app.Request.Url.ToString()))
{
//獲取目的URL
string sendToUrl = reg.Replace(app.Request.Url.ToString(), rules[i].SendTo);
//跟蹤日志
app.Context.Trace.Write("ModuleRewriter", "Rewriting URL to " + sendToUrl);
//地址重寫
app.Context.RewritePath(sendToUrl);
//Temp code for debug
//using (StreamWriter sw = new StreamWriter(@"c:\test\rr.txt", true, System.Text.Encoding.UTF8))
//{
// sw.WriteLine(app.Request.Url.ToString());
// sw.WriteLine("--------------------------------------");
// sw.Close();
//}
//退出for循環
break;
}
}
//結束跟蹤日志
app.Context.Trace.Write("ModuleRewriter", "Exiting ModuleRewriter");
}
}//end class
}
最后在web.config中注冊自定義的Http模塊:
<add name="ModuleRewriter" type="PaoTiao.PTRewriter.ModuleRewriter, PaoTiao.PTRewriter"/>
</httpModules>
四、應用
回到前面的示例, http://wu-jian.cnblogs.com --> /wu-jian/default.aspx
wu-jian所在的位置為域名前綴,或叫二級域名,這就需要在DNS上做一個 *.cnblogs.com 的泛解析。
第二個示例是將目錄解析到某一地址:http://www.cnblogs.com/wu-jian --> /test/url.aspx?p=wu-jian
很明顯,這里的關鍵點在于怎樣讓IIS把這種格式的請求交由.net進程來處理,一旦進入.net framwork,我們就能隨心所欲了。OK,通過如下的操作過程即可:
IIS管理-->站點-->屬性-->主目錄標簽-->配置-->通配符應用程序映射-->插入
1、選擇 C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll
2、不勾選“確認文件是否存在”
五、總結
關于Url Rewrite的介紹很多,第三方組件也很多,比如isapi rewrite、比如iirf,他們通過IIS底層接口實現所以效率更高,Url Rewriter在.Net進程內實現,也是微軟在Framework1.1時代提供的解決方案,其實這篇文章也是多年前寫的,只是最近準備換用IIRF,看著那些在.Net Framework4.0中提示已過期的方法,不得不感嘆時光流水,對以前曾花了時間和精力的東西做個整理和備忘吧,同時希望給有需要的人帶來幫助。