天天看點

利用過濾器Filter和特性Attribute實作對Web API傳回結果的封裝和統一異常處理

利用過濾器Filter和特性Attribute實作對Web API傳回結果的封裝和統一異常處理

在我們開發Web API應用的時候,我們可以借鑒ABP架構的過濾器Filter和特性Attribute的應用,實作對Web API傳回結果的封裝和統一異常處理,本篇随筆介紹利用AuthorizeAttribute實作Web API身份認證,利用ActionFilterAttribute實作對正常Web API傳回結果進行統一格式的封裝,利用ExceptionFilterAttribute實作對接口異常的統一處理,實作我們Web API常用到的幾個通用處理過程。

在我們開發Web API應用的時候,我們可以借鑒ABP架構的過濾器Filter和特性Attribute的應用,實作對Web API傳回結果的封裝和統一異常處理,本篇随筆介紹利用AuthorizeAttribute實作Web API身份認證,利用ActionFilterAttribute實作對正常Web API傳回結果進行統一格式的封裝,利用ExceptionFilterAttribute實作對接口異常的統一處理,實作我們Web API常用到的幾個通用處理過程。

1、Asp.net的Web API過濾器介紹

過濾器主要有這麼幾種:AuthorizationFilterAttribute 權限驗證、ActionFilterAttribute 日志參數驗證等、ExceptionFilterAttribute 異常處理捕獲。

ActionFilter 主要實作執行請求方法體之前(覆寫基類方法OnActionExecuting),和之後的事件處理(覆寫基類方法OnActionExecuted);ExceptionFilter主要實作觸發異常方法(覆寫基類方法OnException)。

ActionFilterAttrubute提供了兩個方法進行攔截:

  • OnActionExecuting和OnActionExecuted,他們都提供了同步和異步的方法。
  • OnActionExecuting方法在Action執行之前執行,OnActionExecuted方法在Action執行完成之後執行。

在使用MVC的時候,ActionFilter提供了一個Order屬性,使用者可以根據這個屬性控制Filter的調用順序,而Web API卻不再支援該屬性。Web API的Filter有自己的一套調用順序規則:

所有Filter根據注冊位置的不同擁有三種作用域:Global、Controller、Action:

  • 通過HttpConfiguration類執行個體下Filters.Add()方法注冊的Filter(一般在App_Start\WebApiConfig.cs檔案中的Register方法中設定)就屬于Global作用域;
  • 通過Controller上打的Attribute進行注冊的Filter就屬于Controller作用域;
  • 通過Action上打的Attribute進行注冊的Filter就屬于Action作用域;

他們遵循了以下規則:

  • 在同一作用域下,AuthorizationFilter最先執行,之後執行ActionFilter
  • 對于AuthorizationFilter和ActionFilter.OnActionExcuting來說,如果一個請求的生命周期中有多個Filter的話,執行順序都是Global->Controller->Action;
  • 對于ActionFilter,OnActionExecuting總是先于OnActionExecuted執行;
  • 對于ExceptionFilter和ActionFilter.OnActionExcuted而言執行順序為Action->Controller->Global;
  • 對于所有Filter來說,如果阻止了請求:即對Response進行了指派,則後續的Filter不再執行。

另外,值得注意的是,由于Web API的過濾器無法改變其順序,那麼它是按照 AuthorizationFilterAttribute -> ActionFilterAttribute -> ExceptionFilterAttribute 這個執行順序來處理的,也就是說授權過濾器執行在前面,再次到自定義的ActionFilter,最後才是異常的過濾器處理。

2、Web API的身份授權過濾器處理

我們通過AuthorizationFilterAttribute 過濾器來處理使用者Web API接口身份,比每次在代碼上進行驗證省事很多。

一般情況下,我們隻要定義類繼承于AuthorizeAttribute即可,由于AuthorizeAttribute是繼承于AuthorizationFilterAttribute,如下所示。

/// <summary>
    /// 驗證Web Api接口使用者身份
    /// </summary>
    public class ApiAuthorizeAttribute : AuthorizeAttribute
    {
            ...........
     }      
利用過濾器Filter和特性Attribute實作對Web API傳回結果的封裝和統一異常處理

 而一般情況下,我們隻需要重寫bool IsAuthorized(HttpActionContext actionContext) 方法,實作授權處理邏輯即可。

利用過濾器Filter和特性Attribute實作對Web API傳回結果的封裝和統一異常處理

 我們在CheckToken的主要邏輯裡面,主要對token令牌進行反向解析,并判斷使用者身份是否符合,如果不符合抛出異常,就會切換到異常處理器裡面了。

利用過濾器Filter和特性Attribute實作對Web API傳回結果的封裝和統一異常處理

  然後在Web API控制器中,需要授權通路的Api控制器定義即可,我們可以把它放到基類裡面聲明這個過濾器特性。

利用過濾器Filter和特性Attribute實作對Web API傳回結果的封裝和統一異常處理

 那麼所有Api接口的通路,都會檢查使用者的身份了。

2、自定義過濾器特性ActionFilterAttribute 的處理

這個ActionFilterAttribute 主要用于攔截使用者通路控制器方法的處理過程,前面說到,OnActionExecuting方法在Action執行之前執行,OnActionExecuted方法在Action執行完成之後執行。

那麼我們可以利用它進行函數AOP的處理了,也就是在執行前,執行後進行日志記錄等,還有就是正常的參數檢查、結果封裝等,都可以在這個自定義過濾器中實作。

我們定義一個類WrapResultAttribute來标記封裝結果,定義一個類DontWrapResultAttribute來标記不封裝結果。

/// <summary>
    /// 用于判斷Web API需要包裝傳回結果.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method)]
    public class WrapResultAttribute : ActionFilterAttribute
{

}

    /// <summary>
    /// 用于判斷Web API不需要包裝傳回結果.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method)]
    public class DontWrapResultAttribute : WrapResultAttribute
{

}      

這個處理方式是借用ABP架構中這兩個特性的處理邏輯。

利用,對于擷取使用者身份令牌的基礎操作接口,我們可以不封裝傳回結果,如下标記所示。

利用過濾器Filter和特性Attribute實作對Web API傳回結果的封裝和統一異常處理

  那麼執行後,傳回的結果如下所示,就是正常的TokenResult對象的JSON資訊

{
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIxIiwiaWF0IjoxNjE3MjY0MDQ4LCJqdGkiOiI0NTBjZmY3OC01OTEwLTQwYzUtYmJjMC01OTQ0YzNjMjhjNTUiLCJuYW1lIjoiYWRtaW4iLCJjb3JwaWQiOiI2IiwiY2hhbm5lbCI6IjAiLCJzaGFyZWRrZXkiOiIxMjM0YWJjZCJ9.Umv4j80Sj6BnoCCGO5LrnyddwtfqU5a8Jii92SjPApw",
    "expires_in": 604800
}      

如果取消這個DontWrapResult的标記,那麼它就繼承基類BaseApiController的WrapResult的标記定義了。

/// <summary>
    /// 所有接口基類
    /// </summary>
    [ExceptionHandling]
    [WrapResult]
    public class BaseApiController : ApiController      

那麼接口定義不變,但是傳回的okenResult對象的JSON資訊已經經過包裝了。

{
    "result": {
        "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIxIiwiaWF0IjoxNjE3MjY0NDQ5LCJqdGkiOiJmZTAzYzhlNi03NGVjLTRjNmEtYmMyZi01NTU3MjFiOTM1NDEiLCJuYW1lIjoiYWRtaW4iLCJjb3JwaWQiOiI2IiwiY2hhbm5lbCI6IjAiLCJzaGFyZWRrZXkiOiIxMjM0YWJjZCJ9.9B4dyoE9YTisl36A-w_evLs2o8raopwvDUIr2LxhO1c",
        "expires_in": 604800
    },
    "targetUrl": null,
    "success": true,
    "error": null,
    "unAuthorizedRequest": false,
    "__api": true
}      

這個JSON格式是我們一個通用的接口傳回,其中Result裡面定義了傳回的資訊,而Error裡面則定義一些錯誤資訊(如果有錯誤的話),而success則用于判斷是否執行成功,如果有錯誤異常資訊,那麼success傳回為false。

這個通用傳回的定義,是依照ABP架構的傳回格式進行調整的,可以作為我們普通Web API的一個通用傳回結果的處理。

前面提到過ActionFilterAttribute自定義處理過程,在控制器方法完成後,我們對傳回的結果進行進一步的封裝處理即可。

我們需要重寫邏輯實作OnActionExecuted的函數

 在做包裝傳回結果之前,我們需要判斷是否方法或者控制器設定了不包裝的标記DontWrapResultAttribute。

public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
        {
            //如果有異常,則退出交給Exception的通用處理
            if (actionExecutedContext.Exception != null)
                return;

            //正常完成,那麼判斷是否需要包裝結果輸出,如果不需要則傳回
            var dontWrap = false;
            var actionContext = actionExecutedContext.ActionContext;
            if (actionContext.ActionDescriptor is ReflectedHttpActionDescriptor actionDesc)
            {
                //判斷方法是否包含DontWrapResultAttribute
                dontWrap = actionDesc.MethodInfo.GetCustomAttributes(inherit: false)
                    .Any(a => a.GetType().Equals(typeof(DontWrapResultAttribute)));

                if (dontWrap) return;
            }
            if (actionContext.ControllerContext.ControllerDescriptor is HttpControllerDescriptor controllerDesc)
            {
                //判斷控制器是否包含DontWrapResultAttribute
                dontWrap = controllerDesc.GetCustomAttributes<Attribute>(inherit: true)
                  .Any(a => a.GetType().Equals(typeof(DontWrapResultAttribute)));

                if (dontWrap) return;
            }      

上述代碼也就是如果找到方法或者控制器有定義DontWrapResultAttribute的,就不要包裝結果,否則下一步就是對結果進行封裝了

//需要包裝,那麼就包裝輸出結果
            AjaxResponse result = new AjaxResponse();
            // 狀态代碼
            var statusCode = actionContext.Response.StatusCode;
            // 讀取傳回的内容
            var content = actionContext.Response.Content.ReadAsAsync<object>().Result;
            // 請求是否成功
            result.Success = actionContext.Response.IsSuccessStatusCode;

            // 重新封裝回傳格式
            actionExecutedContext.Response = new HttpResponseMessage(statusCode)
            {
                Content = new ObjectContent<AjaxResponse>(
                   new AjaxResponse(content), JsonFomatterHelper.GetFormatter())
            };      

其中AjaxResponse是參考ABP架構裡面傳回結果的類定義處理的。

public abstract class AjaxResponseBase
    {
        public string TargetUrl { get; set; }

        public bool Success { get; set; }

        public ErrorInfo Error { get; set; }

        public bool UnAuthorizedRequest { get; set; }

        public bool __api { get; } = true;
    }      
[Serializable]
    public class AjaxResponse<TResult> : AjaxResponseBase
    {
        public TResult Result { get; set; }

      }      

 3、異常處理過濾器ExceptionFilterAttribute 

 前面介紹到,Web API的過濾器無法改變其順序,它是按照 AuthorizationFilterAttribute -> ActionFilterAttribute -> ExceptionFilterAttribute 這個執行順序來處理的,也就是說授權過濾器執行在前面,再次到自定義的ActionFilter,最後才是異常的過濾器處理。

異常處理過濾器,我們定義後,可以統一處理和封裝異常資訊,而我們隻需要實作OnException的方法即可。

/// <summary>
    /// 自定義異常處理
    /// </summary>
    public class ExceptionHandlingAttribute : ExceptionFilterAttribute
    {
        /// <summary>
        /// 統一對調用異常資訊進行處理,傳回自定義的異常資訊
        /// </summary>
        /// <param name="context">HTTP上下文對象</param>
        public override void OnException(HttpActionExecutedContext context)
        {
        }
    }      

完整的處理過程代碼如下所示。

/// <summary>
    /// 自定義異常處理
    /// </summary>
    public class ExceptionHandlingAttribute : ExceptionFilterAttribute
    {
        /// <summary>
        /// 統一對調用異常資訊進行處理,傳回自定義的異常資訊
        /// </summary>
        /// <param name="context">HTTP上下文對象</param>
        public override void OnException(HttpActionExecutedContext context)
        {
            //擷取方法或控制器對應的WrapResultAttribute屬性
            var actionDescriptor = context.ActionContext.ActionDescriptor;
            var wrapResult = actionDescriptor.GetCustomAttributes<WrapResultAttribute>(inherit: true).FirstOrDefault()
                ?? actionDescriptor.ControllerDescriptor.GetCustomAttributes<WrapResultAttribute>(inherit: true).FirstOrDefault();

            //如設定,記錄異常資訊
            if (wrapResult != null && wrapResult.LogError)
            {
                LogHelper.Error(context.Exception);
            }

            var statusCode = GetStatusCode(context, wrapResult.WrapOnError);
            if (!wrapResult.WrapOnError)
            {
                context.Response = new HttpResponseMessage(statusCode) { 
                    Content = new StringContent(context.Exception.Message.ToJson())
                };
                context.Exception = null; //Handled!
                return;
            }

            //使用AjaxResponse包裝結果
            var content = new ErrorInfo(context.Exception.Message/*, context.Exception.StackTrace*/);
            var isAuth = context.Exception is AuthorizationException;
            context.Response = new HttpResponseMessage(statusCode)
            {
                Content = new ObjectContent<AjaxResponse>(
                   new AjaxResponse(content, isAuth), JsonFomatterHelper.GetFormatter())
            };
            context.Exception = null; //Handled!
        }      

這樣我們在BaseApiController裡面聲明即可。

利用過濾器Filter和特性Attribute實作對Web API傳回結果的封裝和統一異常處理

  這樣,一旦程式處理過程中,有錯誤抛出,都會統一到這裡進行處理,有異常的傳回JSON如下所示。

利用過濾器Filter和特性Attribute實作對Web API傳回結果的封裝和統一異常處理

本篇随筆介紹利用AuthorizeAttribute實作Web API身份認證,利用ActionFilterAttribute實作對正常Web API傳回結果進行統一格式的封裝,利用ExceptionFilterAttribute實作對接口異常的統一處理,實作我們Web API常用到的幾個通用處理過程。