對於Api的使用者來說,如果每一個Api的輸入或輸出格式都不一致,會增加使用上的複雜度,而且必須依照每一個Api來客製化傳輸或接收資料的方法,讓使用起來不太方便。因此提供Api輸入輸出的制式規格也是很重要的,讓使用者在使用每一個Api時,都可以使用同一個Scenario來思考,遇到問題也比較容易解決,也可以減低我們在營運和維護上的困難度。
至於異常處理對於使用者來說也是很重要的一環,我們所提供的資訊如果太過於細節,可能會造成資安的風險,相反得如果提供的資訊太少,又會造成串接時不容易排除異常,可能會造成更多無形的時間消耗,所以今天也將和大家介紹如何有效的來提供異常資訊。
※使用同一份輸入格式
在輸入格式的部分,可以參考之前的文章,我們可以藉由在Action加上AuthorzieByToken屬性來限制使用者必須要帶入Token等資訊來存取資料,同時也是統一了輸入的規格,並透過客製化ModelBinder來處理輸入的格式投射到輸入參數的邏輯。
※統一輸出資料樣式
在之前的實作之中,我們都是直接將查詢成功的資料轉成Json回應給使用者,而在接下來希望可以不論是執行成功或失敗,都是透過統一的Json格式來回傳訊息給使用者。因此我們將會都過一個類似Adapter模式的方法,將回傳的資料多包一層Wrapper,在提供給使用者。
在ViewModels專案產生通用的回應格式,ApiResultEntity
public class ApiResultEntity
{
public string Status { get; set; }
public object Data { get; set; }
public string ErrorMessage { get; set; }
}
在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;
}
}
}
在FilterConfig加入ApiResultAttribute
filters.Add(new ApiResultAttribute());
重新查詢資料,發現已經統一格式
※統一錯誤回應訊息格式
在Asp.Net MVC中,預設都會有一個HandleErrorAttribute來攔截所有Controller發生的異常,我們可以透過override Attribute,來讓異常也可以透過Json的制式規格來回傳給使用者。
在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;
}
}
將FilterConfig的HandleErrorAttribute移除,改為使用Attribute
filters.Add(new ApiErrorHandleAttribute());
由於我們現在可以透過制式規格回傳錯誤訊息,修改一下我們的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);
}
}
}
嘗試產生錯誤訊息,我們可以看到錯誤內容已經跟成功一樣,透過統一規格來回應給使用者端
※隱藏敏感錯誤資訊
雖然我們現在可以使用統一的錯誤訊息回傳格式,但目前我們是直接將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來取代它。
在Utility的Extensions專案,使用Nuget新增Enterprise Library Exception Handling Block
在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);
}
}
}
修改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());
}
}
在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);
}
}
在Global.asax啟動
ExceptionHandlingConfig.Initialize();
設定Config檔,決定異常處理策略
註: 必須先安裝Microsoft.Practices.EnterpriseLibrary.ConfigConsoleV6.vsix
新增Exception Handling Settings
我們將使用白名單的方式來透露錯誤資訊,因此先設定所有異常的錯誤訊息,選擇新增Handler
設定要替換為哪種Exception,並設定錯誤訊息為Something went wrong while processing your request. Please contact system adminstrator.
選擇Application Exception
繼續設定我們的異常處理策略,當發生資料庫錯誤時,我們選擇不曝露資訊,只回傳Database Failure,而若是輸入資料驗證失敗,則完整的回傳錯誤資訊
當發生資料庫相關的異常時,已經不會顯示敏感資訊了
相反的若是資料驗證的錯誤,我們可以顯示明確的錯誤訊息
透過Exception Handling Block,不但可以讓我們過濾並決定要顯示的異常訊息,就算是線上的網站也可以透過修改Config檔來即時調整,讓我們的異常處理變得十分有彈性!
※本日小結
我們根據Asp.Net MVC所提供的ActionFilter,再加上簡單的Adapter Pattern,就完成了簡單的異常處理策略,並隨時可以透過修改config檔的方式增減異常處理的邏輯,預設我們也選擇使用了白名單策略,讓使用者不會直接面對到系統中的敏感訊息,而根據使用上的需求,再逐漸開放可以透露的異常資訊,增加使用上的便利性!關於今天的內容,歡迎大家一起討論喔^_^