返回到目录:晒晒我的Ajax服务端框架

我的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找到方法名,也会忽略本次请求。

返回到目录:晒晒我的Ajax服务端框架

点击此处进入示例展示及下载页面

posted on 2011-05-02 13:42  Fish Li  阅读(5877)  评论(12编辑  收藏