愛流浪的小風

技術隨手寫

使用Asp.Net MVC打造Web Api (16) - 統一輸入/出格式以及異常處理策略

| Comments

對於Api的使用者來說,如果每一個Api的輸入或輸出格式都不一致,會增加使用上的複雜度,而且必須依照每一個Api來客製化傳輸或接收資料的方法,讓使用起來不太方便。因此提供Api輸入輸出的制式規格也是很重要的,讓使用者在使用每一個Api時,都可以使用同一個Scenario來思考,遇到問題也比較容易解決,也可以減低我們在營運和維護上的困難度。

至於異常處理對於使用者來說也是很重要的一環,我們所提供的資訊如果太過於細節,可能會造成資安的風險,相反得如果提供的資訊太少,又會造成串接時不容易排除異常,可能會造成更多無形的時間消耗,所以今天也將和大家介紹如何有效的來提供異常資訊。

使用同一份輸入格式

在輸入格式的部分,可以參考之前的文章,我們可以藉由在Action加上AuthorzieByToken屬性來限制使用者必須要帶入Token等資訊來存取資料,同時也是統一了輸入的規格,並透過客製化ModelBinder來處理輸入的格式投射到輸入參數的邏輯。

統一輸出資料樣式

在之前的實作之中,我們都是直接將查詢成功的資料轉成Json回應給使用者,而在接下來希望可以不論是執行成功或失敗,都是透過統一的Json格式來回傳訊息給使用者。因此我們將會都過一個類似Adapter模式的方法,將回傳的資料多包一層Wrapper,在提供給使用者。

  1. 在ViewModels專案產生通用的回應格式,ApiResultEntity

    public class ApiResultEntity
    {
        public string Status { get; set; }
    
        public object Data { get; set; }
    
        public string ErrorMessage { get; set; }
    }    
    
  2. 在WebSite新增ApiResultAttribute,回傳資料前重新包裝

    public class ApiResultAttribute : ActionFilterAttribute
    {
        public override void OnResultExecuting(ResultExecutingContext filterContext)
        {
            if (filterContext.Result is JsonResult)
            {
                var result = filterContext.Result as JsonResult;
                var data = result.Data;
    
                ApiResultEntity entity = new ApiResultEntity();
                entity.Status = ApiStatusEnum.Success.ToString();
                entity.Data = data;
    
                result.Data = entity;
            }            
        }
    }
    
  3. 在FilterConfig加入ApiResultAttribute

    filters.Add(new ApiResultAttribute());
    
  4. 重新查詢資料,發現已經統一格式

統一錯誤回應訊息格式

在Asp.Net MVC中,預設都會有一個HandleErrorAttribute來攔截所有Controller發生的異常,我們可以透過override Attribute,來讓異常也可以透過Json的制式規格來回傳給使用者。

  1. 在WebSite新增ApiErrorHandleAttribute

    public class ApiErrorHandleAttribute : HandleErrorAttribute, IExceptionFilter
    {
        public override void OnException(ExceptionContext filterContext)
        {
            //If message is null or empty, then fill with generic message
            var errorMessage = filterContext.Exception.Message;           
    
            //Set the response status code to 500
            filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
    
            //Needed for IIS7.0
            filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
    
            var result = new ApiResultEntity()
            {
                Status = ApiStatusEnum.Failure.ToString(),
                ErrorMessage = errorMessage
            };
    
            filterContext.Result = new JsonResult
            {
                Data = result,
                ContentEncoding = System.Text.Encoding.UTF8,
                JsonRequestBehavior = JsonRequestBehavior.AllowGet
            };
    
            //Let the system know that the exception has been handled
            filterContext.ExceptionHandled = true;
        }
    }
    
  2. 將FilterConfig的HandleErrorAttribute移除,改為使用Attribute

    filters.Add(new ApiErrorHandleAttribute());
    
  3. 由於我們現在可以透過制式規格回傳錯誤訊息,修改一下我們的ValidateRequestEntityAttribute,改為驗證失敗時丟出Exception,讓後面來處理異常訊息的呈現

    public class ValidateRequestEntityAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var modelState = filterContext.Controller.ViewData.ModelState;
            if (!filterContext.Controller.ViewData.ModelState.IsValid)
            {
                string errorMessages = string.Join("; ", modelState.Values
                                                              .SelectMany(x => x.Errors)
                                                              .Select(x => x.ErrorMessage));
    
                throw new ValidateEntityFailureException(errorMessages);                   
            }
        }
    }
    
  4. 嘗試產生錯誤訊息,我們可以看到錯誤內容已經跟成功一樣,透過統一規格來回應給使用者端

隱藏敏感錯誤資訊

雖然我們現在可以使用統一的錯誤訊息回傳格式,但目前我們是直接將Exception的Message回傳給使用者,萬一是資料庫發生錯誤時,我們很有可能會直接將資料庫的敏感性錯誤訊息曝露給使用者,這是很不安全的,例如

    {
        "Status": "Failure",
        "Data": null,
        "ErrorMessage": "The INSERT statement conflicted with the FOREIGN KEY constraint "FK_dbo.Products_dbo.Categories_CategoryId". The conflict occurred in database "ShopContextDB", table "dbo.Categories", column 'Id'."
    }

所以直接將錯誤訊息回傳是不太好的,但如果將所有錯誤訊息都隱藏起來,對使用者的開發來說也是一個很大的困擾。因此我們需要一套異常處理策略模組,來幫助我們過濾Exception,針對每一個Exception決定是否回傳訊息給使用者,或是要隱藏起來,最好還可以動態調整它。

異常處理策略

為了可以適當的回應錯誤訊息給使用者,我們應該根據錯誤訊息的種類來決定可以透露的資訊,今天將介紹如何使用Enterprise Library Exception Handling Block來處理我們的異常策略,透過Config檔設定Exception對應型別的處理方法,可以直接往後送,或是要重新產生一個新的Exception來取代它。

  1. 在Utility的Extensions專案,使用Nuget新增Enterprise Library Exception Handling Block

  2. 在Extensions新增ExceptionHandlingAttribute,處理異常

    public class ExceptionHandlingAttribute : HandleErrorAttribute, IExceptionFilter
    {
        private string policyName;
    
        public ExceptionHandlingAttribute()
            : this("Policy")
        {
        }
    
        public ExceptionHandlingAttribute(string policyName)
        {
            this.policyName = policyName;
        }
    
        public void OnException(ExceptionContext filterContext)
        {
            try
            {
                ExceptionPolicy.HandleException(filterContext.Exception, this.policyName);
            }           
            catch (Exception ex)
            {
                filterContext.Exception = ex;
                base.OnException(filterContext);
            }               
        }
    }
    
  3. 修改filter.config,依照次序指定Attribute(注意ApiErrorHandleAttribute要在最外層

    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new ApiErrorHandleAttribute());
            filters.Add(new ExceptionHandlingAttribute());
            filters.Add(new ApiResultAttribute());
        }
    }
    
  4. 在AppStart新增ExceptionHandlingConfig,設定ExceptionPolicy的來源

    public class ExceptionHandlingConfig
    {
        public static void Initialize()
        {
            //抓取Config檔案中的設定,作為configurationSource
            IConfigurationSource configurationSource = ConfigurationSourceFactory.Create();
            //以Config檔案中的設定,建立ExceptionManager
            ExceptionPolicy.SetExceptionManager(new ExceptionPolicyFactory(configurationSource).CreateManager(), true);
        }
    }
    
  5. 在Global.asax啟動

    ExceptionHandlingConfig.Initialize();
    
  6. 設定Config檔,決定異常處理策略

    註: 必須先安裝Microsoft.Practices.EnterpriseLibrary.ConfigConsoleV6.vsix

  7. 新增Exception Handling Settings

  8. 我們將使用白名單的方式來透露錯誤資訊,因此先設定所有異常的錯誤訊息,選擇新增Handler

  9. 設定要替換為哪種Exception,並設定錯誤訊息為Something went wrong while processing your request. Please contact system adminstrator.

  10. 選擇Application Exception

  11. 繼續設定我們的異常處理策略,當發生資料庫錯誤時,我們選擇不曝露資訊,只回傳Database Failure,而若是輸入資料驗證失敗,則完整的回傳錯誤資訊

  12. 當發生資料庫相關的異常時,已經不會顯示敏感資訊了

  13. 相反的若是資料驗證的錯誤,我們可以顯示明確的錯誤訊息

透過Exception Handling Block,不但可以讓我們過濾並決定要顯示的異常訊息,就算是線上的網站也可以透過修改Config檔來即時調整,讓我們的異常處理變得十分有彈性!

本日小結

我們根據Asp.Net MVC所提供的ActionFilter,再加上簡單的Adapter Pattern,就完成了簡單的異常處理策略,並隨時可以透過修改config檔的方式增減異常處理的邏輯,預設我們也選擇使用了白名單策略,讓使用者不會直接面對到系統中的敏感訊息,而根據使用上的需求,再逐漸開放可以透露的異常資訊,增加使用上的便利性!關於今天的內容,歡迎大家一起討論喔^_^

Comments

comments powered by Disqus