|
如果您希望看到關(guān)鍵字過濾算法的話那么可能就要失望了。博客園中已經(jīng)有不少關(guān)于此類算法的文章(例如這里和這里),雖然可能無法直接滿足特定需求,但是已經(jīng)足夠作為參考使用。而本文的目的,是給出一個(gè)較為完整的關(guān)鍵字過濾功能,也就是將用戶輸入中的敏感字符進(jìn)行替換——這兩者有什么區(qū)別?那么就請繼續(xù)看下去吧。:)
有趣的需求
關(guān)鍵字過濾功能自然無比重要,但是如果要在代碼中對每個(gè)輸入進(jìn)行檢查和替換則會是一件非常費(fèi)神費(fèi)事的事情。尤其是如果網(wǎng)站已經(jīng)有了一定規(guī)模,用戶輸入功能已經(jīng)遍及各處,而急需對所有輸入進(jìn)行關(guān)鍵字過濾時(shí),上述做法更可謂“遠(yuǎn)水解不了近渴”。這時(shí)候,如果有一個(gè)通用的辦法,呼得一下為整站的輸入加上了一道屏障,那該是一件多么愜意的事情。這就是本文希望解決的問題。是不是很簡單?我一開始也這么認(rèn)為,不過事實(shí)上并非那么一帆風(fēng)順,而且在某些特定條件下似乎更是沒有太好的解決方法……
您慢坐,且聽我慢慢道來……
實(shí)現(xiàn)似乎很簡單
數(shù)據(jù)結(jié)構(gòu)中的單向鏈表可謂無比經(jīng)典。有人說:單向鏈表的題目好難啊,沒法逆序查找,很多東西都不容易做。有人卻說:單向鏈表既然只能向一個(gè)方向遍歷,那么變化就會很有限,所以題目不會過于復(fù)雜。老趙覺得后者的說法不無道理。例如在現(xiàn)在的問題上,我們?nèi)绻谝粋€(gè)ASP.NET應(yīng)用程序中做一個(gè)統(tǒng)一的“整站方案”,HttpModule似乎是唯一的選擇。
思路如下:我們在Request Pipeline中最早的階段(BeginRequest)將請求的QueryString和Form集合中的值做過濾,則接下來的ASP.NET處理過程中一切都為“規(guī)范”的文字了。說干就干,不就是替換兩個(gè)NameValueCollection對象中的值嗎?這再簡單不過了:
public class FilterForbiddenWordModule : IHttpModule{ void IHttpModule.Dispose() { } void IHttpModule.Init(HttpApplication context) { context.BeginRequest += new EventHandler(OnBeginRequest); } private static void OnBeginRequest(object sender, EventArgs e) { var request = (sender as HttpApplication).Request; ProcessCollection(request.QueryString); ProcessCollection(request.Form); } private static void ProcessCollection(NameValueCollection collection) { var copy = new NameValueCollection(); foreach (string key in collection.AllKeys) { Array.ForEach( collection.GetValues(key), v => copy.Add(key, ForbiddenWord.Filter(v))); } collection.Clear(); collection.Add(copy); }}
在BeginRequest階段,我們將調(diào)用ProcessCollection將QueryString和Form兩個(gè)NameValueCollection中的值使用ForbiddenWord.Filter方法進(jìn)行處理。ForbiddenWord是一個(gè)靜態(tài)類,其中的Filter方法會將原始字符串中的敏感字符使用“**”進(jìn)行替換。替換方法不在本文的討論范圍內(nèi),因此我們就以如下方式進(jìn)行簡單替換:
public static class ForbiddenWord{ public static string Filter(string original) { return original.Replace("FORBIDDEN_WORD", "**"); }}
看似沒有問題,OK,隨便打開一張頁面看看……
Collection is read-only.
Description: An unhandled exception occurred during the execution of the current web request... Exception Details: System.NotSupportedException: Collection is read-only.
呀,只讀……這是怎么回事?不就是一個(gè)NameValueCollection嗎?在不得不請出.NET Reflector之后,老趙果然發(fā)現(xiàn)其中有貓膩……
public class HttpRequest{ ... public NameValueCollection Form { get { if (this._form == null) { this._form = new HttpValueCollection(); if (this._wr != null) { this.FillInFormCollection(); } this._form.MakeReadOnly(); } if (this._flags[2]) { this._flags.Clear(2); ValidateNameValueCollection(this._form, "Request.Form"); } return this._form; } } ...}
雖然HttpRequest.Form屬性為NameValueCollection類型,但是其中的_form變量事實(shí)上是一個(gè)HttpValueCollection對象。而HttpValueCollection自然是NameValueCollection的子類,而造成其“只讀”的最大原因便是:
[Serializable]internal class HttpValueCollection : NameValueCollection{ ... internal void MakeReadOnly() { base.IsReadOnly = true; } ...}
IsReadOnly是定義在NameValueCollection基類NameObjectCollectionBase上的protected屬性,這意味著如果我們只有編寫一個(gè)如同NameValueCollection或HttpValueCollection般的子類才能直接訪問它,而現(xiàn)在……反射吧,兄弟們。
public class FilterForbiddenWordModule : IHttpModule{ private static PropertyInfo s_isReadOnlyPropertyInfo; static FilterForbiddenWordModule() { Type type = typeof(NameObjectCollectionBase); s_isReadOnlyPropertyInfo = type.GetProperty( "IsReadOnly", BindingFlags.Instance | BindingFlags.NonPublic); } ... private static void ProcessCollection(NameValueCollection collection) { var copy = new NameValueCollection(); foreach (string key in collection.AllKeys) { Array.ForEach( collection.GetValues(key), v => copy.Add(key, ForbiddenWord.Filter(v))); } // set readonly to false. s_isReadOnlyPropertyInfo.SetValue(collection, false, null); collection.Clear(); collection.Add(copy); // set readonly to true. s_isReadOnlyPropertyInfo.SetValue(collection, true, null); } }
現(xiàn)在再打開個(gè)頁面看看,似乎沒事。那么就來體驗(yàn)一下這個(gè)HttpModule的功效吧。我們先準(zhǔn)備一個(gè)空的ASPx頁面,加上以下代碼:
<form id="form1" runat="server"> <ASP:TextBox runat="server" TextMode="MultiLine" /> <ASP:Button runat="server" Text="Click" />form>
打開頁面,在文本框內(nèi)填寫一些敏感字符并點(diǎn)擊按鈕:
嗨,效果似乎還不錯(cuò)!
問題來了
太簡單了,是不?
可惜問題才剛開始:如果業(yè)務(wù)中有些字段不應(yīng)該被替換怎么辦?例如“密碼”。如果我們只做到現(xiàn)在這點(diǎn),那么密碼“let-us-say-shit”和“let-us-say-fuck”則會被認(rèn)為相同——服務(wù)器端邏輯接收到的都是“let-us-say-**”。也就是說,我們必須提供一個(gè)機(jī)制,讓上面的HttpModule可以“忽略”掉某些內(nèi)容。
如果是其他一些解決方案,我們可以在客戶端進(jìn)行一些特殊標(biāo)記。例如在客戶端增加一個(gè)“-noffw-password”字段來表示忽略對“password”字段的過濾。不過根據(jù)著名的“Don't trust the client”原則,這種做法應(yīng)該是第一個(gè)被否決掉的。試想,如果某些哥們發(fā)現(xiàn)了這一點(diǎn)(別說“不可能”),那么想要繞開這種過濾方式實(shí)在是一件非常容易的事情。不過我們應(yīng)該可以把這種“約定”直接運(yùn)用在字段名上。例如原本我們?nèi)绻∶麨?ldquo;password”的字段,現(xiàn)在直接使用“-noffw-password”,而HttpModule發(fā)現(xiàn)了這種前綴就會放它一馬。由于字段的命名完全是由服務(wù)器端決定,因此采取這種方式之后客戶端的惡人們就無法繞開我們的過濾了。
還有一種情況就是我們要對某些特定的字段采取一些特殊的過濾方式。例如,之前相當(dāng)長的一段時(shí)間內(nèi)我認(rèn)為在服務(wù)器端反序列化一段JSON字符串是非常不合理的,不過由于AJAX幾乎成了事實(shí)標(biāo)準(zhǔn),亦或是現(xiàn)在的Web應(yīng)用程序經(jīng)常需要傳遞一些結(jié)構(gòu)復(fù)雜的對象,JSON格式已經(jīng)越來越多地被服務(wù)器端所接受。假如一個(gè)字段是表示一個(gè)JSON字符串,那么首先我們只應(yīng)該對它的“值”進(jìn)行過濾,而忽略其中的“鍵”。對于這種字段,我們依舊可以使用如上的命名約定來進(jìn)行忽略。例如,我們可以使用-json-data的方法來告訴服務(wù)器端這個(gè)字段應(yīng)該被當(dāng)作JSON格式進(jìn)行處理。
如何?
其實(shí)問題遠(yuǎn)沒有解決——盡情期待《一個(gè)較完整的關(guān)鍵字過濾解決方案(下)》。
NET技術(shù):一個(gè)較完整的關(guān)鍵字過濾解決方案(上),轉(zhuǎn)載需保留來源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請第一時(shí)間聯(lián)系我們修改或刪除,多謝。