|
問題遠沒結束
上面的問題解決了沒有?哦哦,我是指采取命名約定的方式來改變過濾行為。當然有問題,不過我這里提一下比較重要的兩個:
首先,就是“改名”這種行為——究竟是否方便?還記得我們的需求嗎(提示一下:方便、通用……)?如果采取上面的命名約定方案,我們可能就需要在頁面的前端和后端都不斷地改名,一會兒加-noffw,一會兒加-json。如果項目只由您來負責這還好辦,只是麻煩一些,但是如果您的團隊中的前臺開發人員性格古怪,固執己見,不愿配合怎么辦(打架我喜歡,可惜不能直接解決問題)?再者,假如您除了一個FilterForbiddenWordModule之外還有類似的“FilterScriptInjectionModule”怎么辦(別真寫這么一個HttpModule,不合適,老趙只是想不出一個恰當的例子了)?如果兩個Module都采取命名約定的方式,那么如何制定一個兩者能同時認同的約定就也是個麻煩事。
再者,命名真是我們可以控制的嗎?某些情況下好說,但是假如您在使用WebForms中的控件怎么辦?WebForm中的一個重要特性就是用過Naming Container來避免客戶端ID的沖突。假設我們的頁面是放在一個Master Page中ID為Main的ContentPlaceHolder中,那么ID為txtPassword的文本框在客戶端里生成的HTML便會如下所示——那么我們又能有什么辦法可以做到“命名約定”嗎?
<input name="ctl00$Main$txtPassword" id="ctl00_Main_txtPassword">input>
嘿,看來這種命名約定的方式有時候真不是那么通用啊。那么我就來設法解決WebForm這個問題。
其實如果要解決WebForm這個問題,說白了就是要設法可以讓服務器端明確指定一些字段的處理方式。這種“特殊”則意味著對于過濾方式的判斷必須與特定的Page——泛化一下,HttpHandler進行綁定。這里我先談一下我的第一個想法:使用Custom Attribute進行標記的方式。我們構造一個FilterForbiddenWordAttribute,其中包含一個抽象GetFilterType方法根據key來指定過濾方式:
public enum FilterForbiddenWordType{ Ignored, Normal, Json, Html}public abstract class FilterForbiddenWordAttribute : Attribute{ public abstract FilterForbiddenWordType GetFilterType(string key);}
我們如果有特別的需求,就可以通過定義一個FilterForbiddenWordHandlerAttribute的子類,重載GetFilterType方法,然后標記在HttpHandler上。如下:
public class DefaultFilterForbiddenWordAttribute : FilterForbiddenWordAttribute{ public override FilterForbiddenWordType GetFilterType(string key) { if (key.EndsWith("txtPassword")) { return FilterForbiddenWordType.Ignored; } return FilterForbiddenWordType.Normal; }}[DefaultFilterForbiddenWord]public partial class Default : System.Web.UI.Page{ ...}
當然,我們還需要對FilterForbiddenWordModule進行一些修改才能使之生效(朋友們可以先不要看代碼,想想這次改變的關鍵在哪里?):
public class FilterForbiddenWordModule : IHttpModule{ ... void IHttpModule.Init(HttpApplication context) { context.PostMapRequestHandler += new EventHandler(OnPostMapRequestHandler); } private static void OnPostMapRequestHandler(object sender, EventArgs e) { var context = (sender as HttpApplication).Context; var handlerType = context.Handler.GetType(); var filter = ((FilterForbiddenWordAttribute[])handlerType.GetCustomAttributes( typeof(FilterForbiddenWordAttribute), true)).FirstOrDefault(); ProcessCollection(context.Request.QueryString, filter); ProcessCollection(context.Request.Form, filter); } private static void ProcessCollection( NameValueCollection collection, FilterForbiddenWordAttribute filter) { var copy = new NameValueCollection(); foreach (string key in collection.AllKeys) { var filterType = (filter == null) ? FilterForbiddenWordType.Normal : filter.GetFilterType(key); Array.ForEach( collection.GetValues(key), v => copy.Add(key, ForbiddenWord.Filter(v, filterType))); } ... }}
修改示例。例如我們在頁面上放置兩個文本框txtPassword和txtNormal:
<ASP:TextBox ID="txtPassword" runat="server" TextMode="MultiLine" /><ASP:TextBox ID="txtNormal" runat="server" TextMode="MultiLine" /><ASP:Button ID="Button1" runat="server" Text="Click" />
點擊,效果不言而喻:
公布答案:因為我們需要等到確認了HttpHandler類型才能獲得FilterForbiddenWordAttribute標記信息,所以這次更新的關鍵是我們必須推遲進行過濾的時機。推遲到哪個階段?自然是能夠確定HttpHandler類型的最早時機,PostMapRequestHandler。我們通過反射來獲取Handler類型上的FilterForbiddenWordAttribute子類的信息,作為Filter傳入帶有額外參數的ProcessCollection方法中。ProcessCollection方法內部會調用根據filter參數來確定某個key的過濾方式:正常(當作純文本進行過濾)、忽略(不過濾)、JSON(只過濾JSON內元素的值)以及HTML(忽視tag和attribute,并考慮文字內的HTML Encode)。其余不變。
順便說一句,以上代碼其實只是為了寫這些內容而在10分鐘內寫好的,不考慮性能、緩存、同步、邊界等情況——因為我相信看了下面的文字您一定會拋棄這種做法。
繼續改進
上面的做法(相對使用命名約定的方式)改進了什么地方?很簡單,之前提到的命名約定的缺點就是上述做法的優點:
- 不同Page(Http Handler)可以自行指定字段所需要的過濾邏輯。
- 無需前端改名,只需后端標記。
- 避免復雜的命名約定,使多種橫切型的過濾功能可以輕易共存。
真是美妙地嗷嗷的,但是有沒有朋友看出問題來?我提示一下:GetFilterType方法中使用了一個常量字符串txtPassword。
估計有朋友會問:“咦,這有什么問題?”粗看似乎沒有,不過老趙看到代碼中出現常量總是要警惕一番(自覺是個好喜歡):為啥要是txtPassword而不是txtPassWord(一個常見的拼寫錯誤)?為啥代碼中用0而不用-1?這里的問題倒不是說一個常量在代碼中到處使用時最好使用一個const——不不,是readonly字段來代替(為啥用const不太好?)。而是……再提示一下,如果某人將頁面上的txtName文本框改為txtUserName那會出現何種情況?
嗯嗯,那么Attribute中的GetFilterType方法當然還是在判斷一個key是否由txtName結尾,而我們修改后的頁面中Post內容中已經變成了txtUserName,咋整?但是可悲的是,我們尊敬的Attribute,就算你拿刀威脅它它也沒法知道該替換什么啊。唉,那又有誰才能知道呢?不用多想,當然是頁面本身了。
.NET中Custom Attribute的特性深入人心,大大增強了.NET中反射機制的可用性,也因此Kent Beck認為NUnit的設計和使用較JUnit更為優雅。老趙的項目中也到處可見Custom Attribute的存在,寫出的代碼也簡單優美強大地很。不過用多了Custom Attribute也造成了一種思維定勢,一些“附加功能”往往都喜歡往上靠,很多問題往往一個功能出來三秒不到腦子里就浮出一個利用Custom Attribute的解決方案。古語有云,“世界如此美好,我卻如此浮躁,這樣不好,不好……”。事實上ASP.NET框架中已經有了不使用Custom Attribute進行“標記”的現成示例。例如,您知道IRequiresSessionState接口和INamingContainer接口的作用嗎?
如果您翻過IRequriedSessionState和INamingContainer接口的文檔,就會發現它們有個共同的特點——沒有任何成員。這意味著什么呢?這意味著實現了這樣的接口的類,唯一的作用就是“別人知道你實現了這個接口”。有點拗口,對吧?其實就是指,這兩個接口只起到了標記的作用。使用Custom Attribute或使用接口對一個類進行標記和擴展的優劣取舍,我打算用額外的一篇文章來討論這個問題(要不現在大家來Brain Storm一下如何?)。目前,朋友們只需關心一點,如果不用Custom Attirubte而使用接口,我們該如何改寫上面的程序。并且,這種改變帶來了什么好處?
如果在某些情況中,我們也可以把對象本身作為參數傳入Custom Attribute的方法中,Attribute方法內部根據參數的屬性來實現邏輯,可惜的是,Page類內部的控件成員是protected變量,無法從外部訪問。對于我們來說,使Http Handler(即頁面)直接實現某個接口的最大(唯一?)好處,就是讓該接口的成員可以訪問頁面內部的非公開成員了。這點就是問題關鍵,我們現在不必直接使用txtPassword這個常量,而是能夠訪問頁面中的txtPassword控件來獲取它相關的屬性(ID)。不再贅述,修改如下:
public interface IForbiddenWordFilter{ FilterForbiddenWordType GetFilterType(string key);}public partial class Default : System.Web.UI.Page, IForbiddenWordFilter{ ... FilterForbiddenWordType IForbiddenWordFilter.GetFilterType(string key) { if (key.EndsWith(this.txtPassword.ID)) return FilterForbiddenWordType.Ignored; return FilterForbiddenWordType.Normal; }}
至于HttpModule上的修改,相信不會難道您,老趙就不在這里說帖太多代碼浪費帶寬了。可以看出,現在的代碼中已經沒有了txtPassword這個常量,取而代之的是對txtPassword對象ID的訪問。現在如果在ASPx中修改了這個控件的ID,那么在ASPx.cs中的變量也會被重構至對應名字,這大大提高了開發效率,降低了出錯可能。
差點忘說了一句,大家千萬不要忘了對于WebForms模型,有幾個特定的key是不能替換的例如“__VIEWSTATE”和“__VIEWSTATEENCRYPTED”。關于這點,老趙的作法是忽略所有以兩條下劃線作為開頭的Key以保護WebForms模型內部需求。
結合上一篇文章《一個較完整的關鍵字過濾解決方案(上)》,這似乎就是個較為完整的解決方案,不過這個話題結束了嗎?當然沒有。在下一篇文章《一個較完整的關鍵字過濾解決方案(下)》里,我們將討論幾個額外的話題,例如:
- 這個解決方案的適用場合?不適用場合?
- 輸入過濾?輸出過濾?
- 我們一定要使用HttpModule進行過濾嗎?
- 性能?
此外,我想大家在看了這篇文章后來一起思考一些問題,而我對于這些問題的看法也會在下一篇文章中談到:
- 在WebForms模型中,Page即是一個Handler,于是可以實現IForbiddenWordFilter。那么Page里Control所需要過濾的內容呢?動態加載的Control呢?
- 這篇文章的示例里有個陷阱,您看的出是在哪里嗎?
NET技術:一個較完整的關鍵字過濾解決方案(中),轉載需保留來源!
鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播更多信息之目的,如作者信息標記有誤,請第一時間聯系我們修改或刪除,多謝。