我的Ajax服务端框架 - 安全问题
通过前面章节的示例代码,您会发现一个问题:那就是在JS中可以调用所有的C#的方法(理论上是可以调用任何一个程序集中的所有Public类的所有方法)。如果您认为这样做,有安全问题,那么可以订阅事件 OnAjaxCall 来过滤请求。FishWebLib提供的Handler或者Module都有这个事件,您可以统一处理。可参考以下代码:
// Ajax调用的安全检查事件。 FishWebLib.Ajax.AjaxMethodV2Handler.OnAjaxCall += new FishWebLib.Ajax.AjaxCallCheckHandler(AjaxMethodV2Handler_OnAjaxCall);
/// <summary> /// Ajax调用检查 /// </summary> /// <param name="e"></param> static void AjaxMethodV2Handler_OnAjaxCall(FishWebLib.Ajax.AjaxCallEventArgs e) { // ################################################################################################## // 在这里可以做一些在Ajax调用时的安全检查。 // ################################################################################################## // 如果经过您的检查逻辑,不允许一个调用请求,可以做如下处理: //e.IsAllowed = false; //e.DenyMessage = "请求的资源不允许访问。"; // 在本示例中,就不处理了。因为在另一个地方,我仍然有机会处理。 }
AjaxCallEventArgs的定义请见后文。
AjaxMethodV1Handler的安全检查示例
// Ajax调用的安全检查事件。 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); // 在这里,我将只检查要调用的类名是不是以Ajax开头,如果不是,则不允许调用。 if( fileName.StartsWith("Ajax", StringComparison.OrdinalIgnoreCase) == false ) { e.IsAllowed = false; e.DenyMessage = "不允许调用指定的类方法。"; } // 要调用的方法名中URL的查询字符串中,也可以检查到。这里就不检查了。 }
UserControlHandler 和 PageMethodModule 也有 OnAjaxCall 事件,可以按上面的方式来类似处理。
您也可以定义一个统一的安全检查方法,只要符合下面的委托定义即可:
namespace FishWebLib.Ajax { /// <summary> /// AJAX调用发生时的委托类型 /// </summary> /// <param name="e">AjaxCallEventArgs类型的事件参数</param> public delegate void AjaxCallCheckHandler(AjaxCallEventArgs e); /// <summary> /// 发生AJAX调用时的事件参数 /// </summary> public sealed class AjaxCallEventArgs : System.EventArgs { /// <summary> /// 本次请求的HttpContext实例 /// </summary> public HttpContext context; /// <summary> /// 调用类型 /// </summary> public AjaxCallType AjaxCallType; /// <summary> /// 调用是否允许 /// </summary> public bool IsAllowed = true; /// <summary> /// 当设置IsAllowed=false时,可为本成员设置一个用于表示禁止访问的消息。 /// </summary> public string DenyMessage; /// <summary> /// 构造方法 /// </summary> /// <param name="cxt">HttpContext对象</param> /// <param name="type">Ajax调用类型</param> public AjaxCallEventArgs(HttpContext cxt, AjaxCallType type) { this.context = cxt; this.AjaxCallType = type; } } /// <summary> /// AJAX调用类型 /// </summary> public enum AjaxCallType { /// <summary> /// 调用C#方法,由AjaxMethodV1Handler引发 /// </summary> AjaxMethodV1, /// <summary> /// 调用用户控件,由UserControlHandler引发 /// </summary> UserControl, /// <summary> /// 调用页面方法,由PageMethodModule引发 /// </summary> PageMethod, /// <summary> /// 调用C#方法,由AjaxMethodV2Handler引发 /// </summary> AjaxMethodV2 } }
如果上面的处理方式仍不能满足要求,那么请创建自己的ashx处理器,实现您自定义的过滤检查,然后调用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调用时,需要知道要调用哪个类型的哪个方法。 // 在AjaxMethodV2Handler的默认实现中,调用了AjaxClassSearchHelper.Parse, // 这个方法分析URL,并根据指定的类型查找模式,去查找指定的类型,并获取一个方法名称。 // ################################################################################################## // 这里,我们有二种选择: // ################################################################################################## // 1. 自己实现一个 ParseTypeMethodPairFromRequest 的委托并赋值给AjaxMethodV2Handler.ParseFunc,这样做有二个好处: // a. 可以实现自己认为更方便的URL,比如URL:/Classname/MethodName.ext // b. 可以检查指定的类型是否允许被Ajax调用。(###安全检查###) // 2. 保持默认的设置,但需要简单的2个配置AjaxClassSearchHelper。 //FishWebLib.Ajax.AjaxClassSearchHelper.Placeholder = ; // 这个参数这里就不设置了,保持默认值。 FishWebLib.Ajax.AjaxClassSearchHelper.ClassNameSearchPattern = new string[] { // 建议将全部供Ajax调用的类,放在一个命名空间下,这样也可以保证在客户端的JS不至于可以调用所有的类型 // 还可以像Asp.net MVC那样,为所有允许Ajax调用的类,取一个后缀,或者前缀也是可行的。 typeof(MyLab.AjaxService.AjaxOrder).AssemblyQualifiedName.Replace("AjaxOrder", "{0}") }; // #############################################################################################3 // 在本网站中,我们选择第一种方法,但为了方便,我将仍借助于第二种方法来简化实现。 FishWebLib.Ajax.AjaxMethodV2Handler.ParseFunc = MyParseTypeMethodPairFromRequest; } /// <summary> /// 根据当前请求获取要调用的类型及方法名 /// </summary> /// <param name="context"></param> /// <returns></returns> static FishWebLib.Ajax.TypeMethodPair MyParseTypeMethodPairFromRequest(HttpContext context) { // 使用FishWebLib提供的方法,简化实现。 FishWebLib.Ajax.TypeMethodPair result = FishWebLib.Ajax.AjaxClassSearchHelper.Parse(context); if( result != null ) { // 在这里,我还可以检查将要调用的类型和方法是否是允许的。 // 这里的规则很简单:如果不是Ajax开头的类型,将不允许访问 if( result.Type.Name.StartsWith("Ajax") == false ) { // 转向另一个方法的调用,或者返回 null 也是表示禁止访问。 result = new FishWebLib.Ajax.TypeMethodPair(typeof(AppHelper), "DenyAjaxAccess"); } } return result; } /// <summary> /// 禁止Ajax访问时要调用的方法 /// </summary> /// <returns></returns> public static string DenyAjaxAccess() { return "请求的资源不允许访问。"; }
AjaxMethodV1Handler的初始化设置
// 注意:下面的配置指定AjaxMethodV1Handler查找类型的程序集范围。 FishWebLib.Ajax.AjaxMethodV1Handler.AjaxAssemblyName = typeof(AjaxTestClass).Assembly.ToString();
统一的异常处理
// 设置Ajax 调用时的异常事件,处理异常。 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) { // 指示异常已经过处理 e.ExceptionHandled = true; // 异常的处理方式也很简单:把异常写入到响应流,并保存异常。 e.context.Response.Write(e.Exception.GetBaseException().Message); SafeLogException(e.Exception); }
我的Ajax服务端框架 - 实现原理
本文将分别介绍FishWebLib提供的三个Handler及一个Module的实现原理。
1. AjaxMethodV1Handler
AjaxMethodV1Handler的主要实现代码如下:
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); }
从以上代码可以看出,处理器非常简单:根据要请求的文件名,去掉扩展名,当成类名,然后与参数AjaxAssemblyName合并,得到一个类名的完全限定形式, 最后获取要调用类的具体类型,然后把请求交给MethodExecutor.ProcessRequest()来处理,在那里将会从URL的查询字符串中读取参数method, 就可以得到要调用的方法名。有了类型与方法名后,就可以唯一确定一个方法了,最后只需要去调用就可以了。
至于如何调用方法,如何给方法的参数赋值,最后如何处理返回值给客户端,就属于框架本身的事情了。
所有的这一切,对于客户端来说,更是透明的。这些透明的实现也就是框架的意义了。
2. AjaxMethodV2Handler
AjaxMethodV2Handler的主要实现代码如下:
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的实现,可以发现,AjaxMethodV2Handler更简单。最终也是把请求交给MethodExecutor.ProcessRequest()来处理。 其实,这二个处理器与PageMethodModule的实现是比较类似的:获取一个类型和一个方法名,扔给MethodExecutor就完事了。
只是AjaxMethodV2Handler把“获取类型和方法名”的过程交给委托的实现来处理了。
默认的实现使用了:AjaxClassSearchHelper.Parse ,它能拆分这种形式的URL: class.method.xx
AjaxClassSearchHelper定义了二个数据成员:
/// <summary> /// 在URL中用于分隔类名和方法名的特殊字符,默认值:'.' /// </summary> public static char Placeholder = '.'; /// <summary> /// 用于搜索类类型的搜索模式字符串数组。搜索模式通常是一个类型的完全限定字符串中将类名改成{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的主要实现代码如下:
public void ProcessRequest(HttpContext context) { if( AjaxCallChecker.RaiseCheckEvent(context, OnAjaxCall, AjaxCallType.UserControl) == false ) return; string filePath = context.Request.AppRelativeCurrentExecutionFilePath; // 这里不检查指定的用户控件是否存在,如果不存在Asp.net会告诉调用方的。 UcExecutor.ProcessRequest(context, filePath, true); }
从代码可以看出:请求最后是由UcExecutor来处理的。
所以,也可以不使用这个处理器,而是将请求交给C#方法来处理,获取数据后,再去调用UcExecutor.ProcessRequest(),这种做法是符合MVC的设计思想的。
4. PageMethodModule
PageMethodModule的实现与前二个处理器类似,不一样的地方在于它是以Module的形式存在的。
为了能够调用MethodExecutor.ProcessRequest(),它也需要知道一个类型和一个方法名。 有了请求页面地址,就可以知道当前在请求哪个页面,自然也就能获取一个类型了,方法名可以通过从FORM中获取, PageMethodModule会尝试读取FORM中键名为"AjaxPageMethod"对应的值,如果找到,后面的事情就如前面所说的那样处理了。
当然,为了性能,PageMethodModule只会处理POST请求。如果没有从FROM找到方法名,也会忽略本次请求。