在WEB Api中,引入了面向切面編程(AOP)的思想,在某些特定的位置可以插入特定的Filter進行過程攔截處理。引入了這一機制可以更好地踐行DRY(Don’t Repeat Yourself)思想,通過Filter能統一地對一些通用邏輯進行處理,如:許可權校正、參數加解密、參數校正等方面我們都可以利用這一特性進行統一處理,今天我們來介紹Filter的開發、使用以及討論他們的執行順序。
一、Filter的開發和調用
在預設的WebApi中,架構提供了三種Filter,他們的功能和運行條件如下表所示:
Filter 類型 |
實現的介面 |
描述 |
Authorization |
IAuthorizationFilter |
最先啟動並執行Filter,被用作請求許可權校正 |
Action |
IActionFilter |
在Action啟動並執行前、後運行 |
Exception |
IExceptionFilter |
當異常發生的時候運行 |
首先,我們實現一個AuthorizatoinFilter可以用以簡單的許可權控制:
(actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute> verifyResult = actionContext.Request.Headers.Authorization!= && == ; (!= HttpError(
一個簡單的用於使用者驗證的Filter就開發完了,這個Filter要求使用者的請求中帶有Authorization頭並且參數為123456,如果通過則允許存取,不通過則返回401錯誤,並在Content中提示Token不正確。下面,我們需要註冊這個Filter,註冊Filter有三種方法:
第一種:在我們希望進行許可權控制的Action上打上AuthFilterAttribute這個Attribute:
public class PersonController : ApiController { [AuthFilter] public CreateResult Post(CreateUser user) { return new CreateResult() {Id = "123"}; } }
這種方式適合單個Action的許可權控制。
第二種,找到相應的Controller,並打上這個Attribute:
[AuthFilter] public class PersonController : ApiController { public CreateResult Post(CreateUser user) { return new CreateResult() {Id = "123"}; } }
這種方式適合於控制整個Controller,打上這個Attribute以後,整個Controller裡所有Action都獲得了許可權控制。
第三種,找到App_Start\WebApiConfig.cs,在Register方法下加入Filter執行個體:
{ id =
用這種方式適合於控制所有的API,任意Controller和任意Action都接受了這個許可權控制。
在大多數情境中,每個API的許可權驗證邏輯都是一樣的,在這樣的前提下使用全域註冊Filter的方法最為簡單便捷,可這樣存在一個顯而易見的問題:如果某幾個API是不需要控制的(例如登入)怎麼辦?我們可以在這樣的API上做這樣的處理:
[AllowAnonymous]public CreateResult PostLogin(LoginEntity entity){ //TODO:添加驗證邏輯 return new CreateResult() {Id = "123456"};}
我為這個Action打上了AllowAnonymousAttribute,驗證邏輯就放過了這個API而不進行許可權校正。
在實際的開發中,我們可以設計一套類似Session的機制,通過使用者登入來擷取Token,在之後的互動HTTP請求中加上Authorization頭並帶上這個Token,並在自訂的AuthFilterAttribute中對Token進行驗證,一套標準的Token驗證流程就可以實現了。
接下來我們介紹ActionFilter:
ActionFilterAttrubute提供了兩個方法進行攔截:OnActionExecuting和OnActionExecuted,他們都提供了同步和非同步方法。
OnActionExecuting方法在Action執行之前執行,OnActionExecuted方法在Action執行完成之後執行。
我們來看一個應用情境:使用過MVC的同學一定不陌生MVC的模型繫結和模型校正,使用起來非常方便,定義好Entity之後,在需要進行校正的地方可以打上相應的Attribute,在Action開始時檢查ModelState的IsValid屬性,如果校正不通過直接返回View,前端可以解析並顯示未通過校正的原因。而Web API中也繼承了這一方便的特性,使用起來更加方便:
public class CustomActionFilterAttribute : ActionFilterAttribute{ public override void OnActionExecuting(HttpActionContext actionContext) { if (!actionContext.ModelState.IsValid) { actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState); } }}
這個Filter就提供了模型校正的功能,如果未通過模型校正則返回400錯誤,並把相關的錯誤資訊交給調用者。他的使用方法和AuthFilterAttribute一樣,可以針對Action、Controller、全域使用。我們可以用下面一個例子來驗證:
代碼如下:
public class LoginEntity{ [Required(ErrorMessage = "缺少使用者名稱")] public string UserName { get; set; } [Required(ErrorMessage = "缺少密碼")] public string Password { get; set; }}
[AllowAnonymous][CustomActionFilter]public CreateResult PostLogin(LoginEntity entity){ //TODO:添加驗證邏輯 return new CreateResult() {Id = "123456"};}
當然,你也可以根據自己的需要解析ModelState然後用自己的格式將錯誤資訊通過Request.CreateResponse()返回給使用者。
OnActionExecuted方法我在實際工作中使用得較少,目前僅在一次部分響應資料加密的情境下進行過使用,使用方法一樣,讀取已有的響應,並加密後再給出加密後的響應賦值給actionContext.Response即可。
我給大家一個Demo:
public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken){ var key = 10; var responseBody = await actionExecutedContext.Response.Content.ReadAsByteArrayAsync(); //以Byte數組方式讀取Content中的資料 for (int i = 0; i < responseBody.Length; i++) { responseBody[i] = (byte)(responseBody[i] ^ key); //對每一個Byte做異或運算 } actionExecutedContext.Response.Content = new ByteArrayContent(responseBody); //將結果賦值給Response的Content actionExecutedContext.Response.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("Encrypt/Bytes"); //並修改Content-Type}
通過這個方法我們將響應的Content每個Byte都做了一個異或運算,對響應內容進行了一次簡單的加密,大家可以根據自己的需要進行更可靠的加密,如AES、DES或者RSA…通過這個方法可以靈活地對某個Action的處理後的結果進行處理,通過Filter進行響應內容加密有很強的靈活性和通用性,他能擷取當前Action的很多資訊,然後根據這些資訊選擇加密的方式、擷取加密所需的參數等等。如果加密所使用參數對當前執行的Action沒有依賴,也可以採取HttpMessageHandler來進行處理,在之後的教程中我會進行介紹。
最後一個Filter:ExceptionFilter
顧名思義,這個Filter是用來進行異常處理的,當業務發生未處理的異常,我們是不希望使用者接收到黃頁或者其他使用者無法解析的資訊的,我們可以使用ExceptionFilter來進行統一處理:
public class ExceptionFilter : ExceptionFilterAttribute{ public override void OnException(HttpActionExecutedContext actionExecutedContext) { //如果截獲異常為我們自訂,可以處理的異常則通過我們自己的規則處理 if (actionExecutedContext.Exception is DemoException) { //TODO:記錄日誌 actionExecutedContext.Response = actionExecutedContext.Request.CreateResponse( HttpStatusCode.BadRequest, new {Message = actionExecutedContext.Exception.Message}); } else { //如果截獲異常是我沒無法預料的異常,則將通用的返回資訊返回給使用者,避免泄露過多資訊,也便於使用者處理 //TODO:記錄日誌 actionExecutedContext.Response = actionExecutedContext.Request.CreateResponse(HttpStatusCode.InternalServerError, new {Message = "伺服器被外星人拐跑了!"}); } }}
我們定義了一個ExceptoinFilter用於處理未捕獲的異常,我們將異常分為兩類:一類是我們可以預料的異常:如業務參數錯誤,越權等業務異常;還有一類是我們無法預料的異常:如資料庫連接斷開、記憶體溢出等異常。我們通過HTTP Code告知調用者以及用相對固定、友好的資料結構將異常資訊告訴調用者,以便於調用者記錄並處理這樣的異常。
[CustomerExceptionFilter]public class TestController : ApiController{ public int Get(int a, int b) { if (a < b) { throw new DemoException("A必須要比B大!"); } if (a == b) { throw new NotImplementedException(); } return a*b; }}
我們定義了一個Action:在不同的情況下會拋出不同的異常,其中一個異常是我們能夠預料並認為是調用者傳參出錯的,一個是不能夠處理的,我們看一下結果:
在這樣的RestApi中,我們可以預先定義好異常的表現形式,讓調用者可以方便地判斷什麼情況下是出現異常了,然後通過較為統一的異常資訊返回方式讓調用者方便地解析異常資訊,形成統一方便的異常訊息處理機制。
但是,ExceptionFilter只能在成功完成了Controller的初始化以後才能起到捕獲、處理異常的作用,而在Controller初始化完成之前(例如在Controller的建構函式中出現了異常)則ExceptionFilter無能為力。對此WebApi引入了ExceptionLogger和ExceptionHandler處理機制,我們將在之後的文章中進行講解。
二、Filter的執行順序
在使用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範圍;
他們遵循了以下規則:
1、在同一範圍下,AuthorizationFilter最先執行,之後執行ActionFilter
2、對於AuthorizationFilter和ActionFilter.OnActionExcuting來說,如果一個請求的生命週期中有多個Filter的話,執行順序都是Global->Controller->Action;
3、對於ActionFilter,OnActionExecuting總是先於OnActionExecuted執行;
4、對於ExceptionFilter和ActionFilter.OnActionExcuted而言執行順序為Action->Controller->Global;
5、對於所有Filter來說,如果阻止了請求:即對Response進行了賦值,則後續的Filter不再執行。
關於預設情況下的Filter相關知識我們就講這麼一些,如果在文章中有任何不正確的地方或者疑問,歡迎大家為我指出。