一個較完整的關鍵字過濾解決方案(中)
[2] 一個較完整的關鍵字過濾解決方案(中)
問題遠沒結束
上面的問題解決了沒有?哦哦,我是指采取命名約定的方式來改變過濾行為。當然有問題,不過我這里提一下比較重要的兩個:
首先,就是“改名”這種行為——究竟是否方便?還記得我們的需求嗎(提示一下:方便、通用……)?如果采取上面的命名約定方案,我們可能就需要在頁面的前端和后端都不斷地改名,一會兒加-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分鐘內寫好的,不考慮性能、緩存、同步、邊界等情況——因為我相信看了下面的文字您一定會拋棄這種做法。