我的Ajax服務端框架 - 安全問題
通過前面章節(jié)的示例代碼,您會發(fā)現(xiàn)一個問題:那就是在JS中可以調(diào)用所有的C#的方法(理論上是可以調(diào)用任何一個程序集中的所有Public類的所有方法)。如果您認為這樣做,有安全問題,那么可以訂閱事件 OnAjaxCall 來過濾請求。FishWebLib提供的Handler或者Module都有這個事件,您可以統(tǒng)一處理??蓞⒖家韵麓a:
// Ajax調(diào)用的安全檢查事件。 FishWebLib.Ajax.AjaxMethodV2Handler.OnAjaxCall += new FishWebLib.Ajax.AjaxCallCheckHandler(AjaxMethodV2Handler_OnAjaxCall);
/// <summary> /// Ajax調(diào)用檢查 /// </summary> /// <param name="e"></param> static void AjaxMethodV2Handler_OnAjaxCall(FishWebLib.Ajax.AjaxCallEventArgs e) { // ################################################################################################## // 在這里可以做一些在Ajax調(diào)用時的安全檢查。 // ################################################################################################## // 如果經(jīng)過您的檢查邏輯,不允許一個調(diào)用請求,可以做如下處理: //e.IsAllowed = false; //e.DenyMessage = "請求的資源不允許訪問。"; // 在本示例中,就不處理了。因為在另一個地方,我仍然有機會處理。 }
AjaxCallEventArgs的定義請見后文。
AjaxMethodV1Handler的安全檢查示例
// Ajax調(diào)用的安全檢查事件。 FishWebLib.Ajax.AjaxMethodV1Handler.OnAjaxCall += new FishWebLib.Ajax.AjaxCallCheckHandler(AjaxMethodV1Handler_OnAjaxCall);
static void AjaxMethodV1Handler_OnAjaxCall(FishWebLib.Ajax.AjaxCallEventArgs e) { string fileName = System.IO.Path.GetFileNameWithoutExtension(e.context.Request.PhysicalPath); // 在這里,我將只檢查要調(diào)用的類名是不是以Ajax開頭,如果不是,則不允許調(diào)用。 if( fileName.StartsWith("Ajax", StringComparison.OrdinalIgnoreCase) == false ) { e.IsAllowed = false; e.DenyMessage = "不允許調(diào)用指定的類方法。"; } // 要調(diào)用的方法名中URL的查詢字符串中,也可以檢查到。這里就不檢查了。 }
UserControlHandler 和 PageMethodModule 也有 OnAjaxCall 事件,可以按上面的方式來類似處理。
您也可以定義一個統(tǒng)一的安全檢查方法,只要符合下面的委托定義即可:
namespace FishWebLib.Ajax { /// <summary> /// AJAX調(diào)用發(fā)生時的委托類型 /// </summary> /// <param name="e">AjaxCallEventArgs類型的事件參數(shù)</param> public delegate void AjaxCallCheckHandler(AjaxCallEventArgs e); /// <summary> /// 發(fā)生AJAX調(diào)用時的事件參數(shù) /// </summary> public sealed class AjaxCallEventArgs : System.EventArgs { /// <summary> /// 本次請求的HttpContext實例 /// </summary> public HttpContext context; /// <summary> /// 調(diào)用類型 /// </summary> public AjaxCallType AjaxCallType; /// <summary> /// 調(diào)用是否允許 /// </summary> public bool IsAllowed = true; /// <summary> /// 當設置IsAllowed=false時,可為本成員設置一個用于表示禁止訪問的消息。 /// </summary> public string DenyMessage; /// <summary> /// 構造方法 /// </summary> /// <param name="cxt">HttpContext對象</param> /// <param name="type">Ajax調(diào)用類型</param> public AjaxCallEventArgs(HttpContext cxt, AjaxCallType type) { this.context = cxt; this.AjaxCallType = type; } } /// <summary> /// AJAX調(diào)用類型 /// </summary> public enum AjaxCallType { /// <summary> /// 調(diào)用C#方法,由AjaxMethodV1Handler引發(fā) /// </summary> AjaxMethodV1, /// <summary> /// 調(diào)用用戶控件,由UserControlHandler引發(fā) /// </summary> UserControl, /// <summary> /// 調(diào)用頁面方法,由PageMethodModule引發(fā) /// </summary> PageMethod, /// <summary> /// 調(diào)用C#方法,由AjaxMethodV2Handler引發(fā) /// </summary> AjaxMethodV2 } }
如果上面的處理方式仍不能滿足要求,那么請創(chuàng)建自己的ashx處理器,實現(xiàn)您自定義的過濾檢查,然后調(diào)用FishWebLib.Ajax.MethodExecutor中的以下方法:
public static void ProcessRequest(HttpContext context, Type type, string method)
我的Ajax服務端框架 - 初始化設置
請參考以下代碼:(在演示程序的AppHelper.cs中可以找到)
AjaxMethodV2Handler的初始化設置
/// <summary> /// 設置AjaxMethodV2Handler查找類型時的工作方式。 /// </summary> static void SetAjaxClassSearchMode() { // 這里先說明一下: // 當AjaxMethodV2Handler被Asp.net調(diào)用時,需要知道要調(diào)用哪個類型的哪個方法。 // 在AjaxMethodV2Handler的默認實現(xiàn)中,調(diào)用了AjaxClassSearchHelper.Parse, // 這個方法分析URL,并根據(jù)指定的類型查找模式,去查找指定的類型,并獲取一個方法名稱。 // ################################################################################################## // 這里,我們有二種選擇: // ################################################################################################## // 1. 自己實現(xiàn)一個 ParseTypeMethodPairFromRequest 的委托并賦值給AjaxMethodV2Handler.ParseFunc,這樣做有二個好處: // a. 可以實現(xiàn)自己認為更方便的URL,比如URL:/Classname/MethodName.ext // b. 可以檢查指定的類型是否允許被Ajax調(diào)用。(###安全檢查###) // 2. 保持默認的設置,但需要簡單的2個配置AjaxClassSearchHelper。 //FishWebLib.Ajax.AjaxClassSearchHelper.Placeholder = ; // 這個參數(shù)這里就不設置了,保持默認值。 FishWebLib.Ajax.AjaxClassSearchHelper.ClassNameSearchPattern = new string[] { // 建議將全部供Ajax調(diào)用的類,放在一個命名空間下,這樣也可以保證在客戶端的JS不至于可以調(diào)用所有的類型 // 還可以像Asp.net MVC那樣,為所有允許Ajax調(diào)用的類,取一個后綴,或者前綴也是可行的。 typeof(MyLab.AjaxService.AjaxOrder).AssemblyQualifiedName.Replace("AjaxOrder", "{0}") }; // #############################################################################################3 // 在本網(wǎng)站中,我們選擇第一種方法,但為了方便,我將仍借助于第二種方法來簡化實現(xiàn)。 FishWebLib.Ajax.AjaxMethodV2Handler.ParseFunc = MyParseTypeMethodPairFromRequest; } /// <summary> /// 根據(jù)當前請求獲取要調(diào)用的類型及方法名 /// </summary> /// <param name="context"></param> /// <returns></returns> static FishWebLib.Ajax.TypeMethodPair MyParseTypeMethodPairFromRequest(HttpContext context) { // 使用FishWebLib提供的方法,簡化實現(xiàn)。 FishWebLib.Ajax.TypeMethodPair result = FishWebLib.Ajax.AjaxClassSearchHelper.Parse(context); if( result != null ) { // 在這里,我還可以檢查將要調(diào)用的類型和方法是否是允許的。 // 這里的規(guī)則很簡單:如果不是Ajax開頭的類型,將不允許訪問 if( result.Type.Name.StartsWith("Ajax") == false ) { // 轉(zhuǎn)向另一個方法的調(diào)用,或者返回 null 也是表示禁止訪問。 result = new FishWebLib.Ajax.TypeMethodPair(typeof(AppHelper), "DenyAjaxAccess"); } } return result; } /// <summary> /// 禁止Ajax訪問時要調(diào)用的方法 /// </summary> /// <returns></returns> public static string DenyAjaxAccess() { return "請求的資源不允許訪問。"; }
AjaxMethodV1Handler的初始化設置
// 注意:下面的配置指定AjaxMethodV1Handler查找類型的程序集范圍。 FishWebLib.Ajax.AjaxMethodV1Handler.AjaxAssemblyName = typeof(AjaxTestClass).Assembly.ToString();
統(tǒng)一的異常處理
// 設置Ajax 調(diào)用時的異常事件,處理異常。 FishWebLib.Ajax.AjaxExceptionHelper.OnAjaxInvokeException += new FishWebLib.Ajax.AjaxExceptionAction(AjaxExceptionHelper_OnAjaxInvokeException);
/// <summary> /// Ajax異常處理 /// </summary> /// <param name="e"></param> static void AjaxExceptionHelper_OnAjaxInvokeException(FishWebLib.Ajax.AjaxExceptionEventArgs e) { // 指示異常已經(jīng)過處理 e.ExceptionHandled = true; // 異常的處理方式也很簡單:把異常寫入到響應流,并保存異常。 e.context.Response.Write(e.Exception.GetBaseException().Message); SafeLogException(e.Exception); }
我的Ajax服務端框架 - 實現(xiàn)原理
本文將分別介紹FishWebLib提供的三個Handler及一個Module的實現(xiàn)原理。
1. AjaxMethodV1Handler
AjaxMethodV1Handler的主要實現(xiàn)代碼如下:
public void ProcessRequest(HttpContext context) { if( string.IsNullOrEmpty(AjaxAssemblyName) ) { AjaxCallChecker.WriteSimpleMessage(context, SR.AjaxAssemblyNameIsNull); return; } if( AjaxCallChecker.RaiseCheckEvent(context, OnAjaxCall, AjaxCallType.AjaxMethodV1) == false ) return; string className = System.IO.Path.GetFileNameWithoutExtension(context.Request.PhysicalPath); Type type = TypeManager.GetTypeByName(string.Concat(className, ", ", AjaxAssemblyName)); if( type == null ) { AjaxExceptionHelper.ProcessException(context, new Exception(string.Format(SR.TypeNotFound, className))); return; } MethodExecutor.ProcessRequest(context, type); }
從以上代碼可以看出,處理器非常簡單:根據(jù)要請求的文件名,去掉擴展名,當成類名,然后與參數(shù)AjaxAssemblyName合并,得到一個類名的完全限定形式, 最后獲取要調(diào)用類的具體類型,然后把請求交給MethodExecutor.ProcessRequest()來處理,在那里將會從URL的查詢字符串中讀取參數(shù)method, 就可以得到要調(diào)用的方法名。有了類型與方法名后,就可以唯一確定一個方法了,最后只需要去調(diào)用就可以了。
至于如何調(diào)用方法,如何給方法的參數(shù)賦值,最后如何處理返回值給客戶端,就屬于框架本身的事情了。
所有的這一切,對于客戶端來說,更是透明的。這些透明的實現(xiàn)也就是框架的意義了。
2. AjaxMethodV2Handler
AjaxMethodV2Handler的主要實現(xiàn)代碼如下:
private static ParseTypeMethodPairFromRequest s_ParseFunc = AjaxClassSearchHelper.Parse; public void ProcessRequest(HttpContext context) { if( AjaxCallChecker.RaiseCheckEvent(context, OnAjaxCall, AjaxCallType.AjaxMethodV2) == false ) return; TypeMethodPair pair = s_ParseFunc(context); if( pair == null ) { AjaxExceptionHelper.ProcessException(context, new Exception(SR.InvalidRequest)); return; } MethodExecutor.ProcessRequest(context, pair.Type, pair.Method); }
如果比較AjaxMethodV1Handler的實現(xiàn),可以發(fā)現(xiàn),AjaxMethodV2Handler更簡單。最終也是把請求交給MethodExecutor.ProcessRequest()來處理。
其實,這二個處理器與PageMethodModule的實現(xiàn)是比較類似的:獲取一個類型和一個方法名,扔給MethodExecutor就完事了。
只是AjaxMethodV2Handler把“獲取類型和方法名”的過程交給委托的實現(xiàn)來處理了。
默認的實現(xiàn)使用了:AjaxClassSearchHelper.Parse ,它能拆分這種形式的URL: class.method.xx
AjaxClassSearchHelper定義了二個數(shù)據(jù)成員:
/// <summary> /// 在URL中用于分隔類名和方法名的特殊字符,默認值:'.' /// </summary> public static char Placeholder = '.'; /// <summary> /// 用于搜索類類型的搜索模式字符串數(shù)組。搜索模式通常是一個類型的完全限定字符串中將類名改成{0} /// </summary> public static string[] ClassNameSearchPattern = null;
可以這樣設置ClassNameSearchPattern:
FishWebLib.Ajax.AjaxClassSearchHelper.ClassNameSearchPattern = new string[] { typeof(MyLab.AjaxService.AjaxOrder).AssemblyQualifiedName.Replace("AjaxOrder", "{0}") };
3. UserControlHandler
UserControlHandler的主要實現(xiàn)代碼如下:
public void ProcessRequest(HttpContext context) { if( AjaxCallChecker.RaiseCheckEvent(context, OnAjaxCall, AjaxCallType.UserControl) == false ) return; string filePath = context.Request.AppRelativeCurrentExecutionFilePath; // 這里不檢查指定的用戶控件是否存在,如果不存在Asp.net會告訴調(diào)用方的。 UcExecutor.ProcessRequest(context, filePath, true); }
從代碼可以看出:請求最后是由UcExecutor來處理的。
所以,也可以不使用這個處理器,而是將請求交給C#方法來處理,獲取數(shù)據(jù)后,再去調(diào)用UcExecutor.ProcessRequest(),這種做法是符合MVC的設計思想的。
4. PageMethodModule
PageMethodModule的實現(xiàn)與前二個處理器類似,不一樣的地方在于它是以Module的形式存在的。
為了能夠調(diào)用MethodExecutor.ProcessRequest(),它也需要知道一個類型和一個方法名。
有了請求頁面地址,就可以知道當前在請求哪個頁面,自然也就能獲取一個類型了,方法名可以通過從FORM中獲取,
PageMethodModule會嘗試讀取FORM中鍵名為"AjaxPageMethod"對應的值,如果找到,后面的事情就如前面所說的那樣處理了。
當然,為了性能,PageMethodModule只會處理POST請求。如果沒有從FROM找到方法名,也會忽略本次請求。
Fish Li (李奇峰)
浙公網(wǎng)安備 33010602011771號