系列目錄
過濾器上下文參數
前一節提到了四種MVC內建過濾器,它們無一例外都在關鍵的方法中提供了叫filterContext的參數,儘管它們各自類型不同,但是都繼承自ControllerContext。
其中一個共同的重要屬性是:
public ActionResult Result { get; set;}
Result是唯一通知MVC架構當前Filter執行結果的媒介,也就是說MVC架構總是在必要的時候判斷filterContext.Result,如果Result不為空白就表示可以繼續,否則Result將被執行(因為它是個ActionResult),並且之後的過程將被跳過。在下面的討論中你會逐步理解。
IActionFilter和IResultFilter
IActionFilter和IResultFilter分別表示在Action執行前動作和Action執行後動作。由前一篇的類圖,我們可以看到MVC內建了ActionFilterAttribute同時實現了這兩個介面,只是所有的實現都是虛方法,沒有任何實際的代碼。因此,可以通過繼承ActionFilterAttribute來實現IActionFilter和IResultFilter。除此之外,ActionFilterAttribute還繼承了FilterAttribute,這個Attribute只定義了一個Order屬性。事實上,對於多個相同的過濾器被定義在同一個action或controller上的時候,Order可以對他們的執行順序進行排序。如果沒有指定的話,預設的情況可以通過下面這個例子說明:
[ShowMessage(Message = "A")][ShowMessage(Message = "B")]public ActionResult SomeAction(){Response.Write("Action is running");return Content("Result is running");}
假設上面的ShowMessage繼承自ActionFilterAttribute,並實現了所有的四個方法,那麼將得到下面的輸出(省略了ShowMessage的實現,不過大家可以猜出來):
[BeforeAction B][BeforeAction A]Action is running[AfterAction A][AfterAction B]
[BeforeResult B][BeforeResult A]Result is running[AfterResult A][AfterResult B]
如果加上Order的話,可以改變這種預設的順序:
[ShowMessage(Message = "A", Order = 1)][ShowMessage(Message = "B", Order = 2)]public ActionResult SomeAction(){Response.Write("Action is running");return Content("Result is running");}
輸出:
[BeforeAction A][BeforeAction B]Action is running[AfterAction B][AfterAction A]
[BeforeResult A][BeforeResult B]Result is running[AfterResult B][AfterResult A]
總之,IActionFilter和IResultFilter還是比較容易理解的,但是有個特殊的問題需要注意,如果在執行IActionFilter或IResultFilter的代碼時異常了怎麼辦?拿IActionFilter作說明,書中有這樣一張圖:
這張圖給了我們這樣的資訊,一個Action上面加了3層IActionFilter,當第三層的OnActionExecuting拋出異常後,被第二層的OnActionExecuted捕獲了,而且繼續執行第一層的OnActionExecuted。其中跳過了ActionMethod和第三層的OnActionExecuted。
IResultFilter實際上跟IActionFilter的行為完全相同。
另外,Response.Redirect()方法將拋出ThreadAbortException異常,MVC自行捕獲了這種特殊的異常,使得這種異常實際上不會影響我們,我們大可假裝對此完全不知。下面的代碼和注釋是從InvokeActionMethodFilter方法中截取的,說明了MVC架構在這裡的“用心良苦”。
catch (ThreadAbortException) { // This type of exception occurs as a result of Response.Redirect(), but we special-case so that // the filters don't see this as an error. postContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, false /* canceled */, null /* exception */); filter.OnActionExecuted(postContext); throw;}
這部分的原始碼有個十分精闢的地方,我將寫一篇文章專門解讀這部分源碼,屆時,上述邏輯將更容易理解。
事實上,OutputCacheAttribute是一個IResultFilter,除了一些屬性,它僅僅重寫了OnResultExecuting,關於如何它的詳細使用,參考書中341頁。
IAuthorizationFilter
IAuthorizationFilter用於頁面層級的使用者驗證,AuthorizeAttribute實現了IAuthorizationFilter,下面的代碼是AuthorizeAttribute的核心驗證邏輯,需要通知滿足三個條件才能認證成功:
1、HttpContext.User.Identity.IsAuthenticated必須為true
2、使用者名稱必須一致(注意StringComparer.OrdinalIgnoreCase說明了不區分大小寫)
3、角色必須一致
protected virtual bool AuthorizeCore(HttpContextBase httpContext) {if (httpContext == null) { throw new ArgumentNullException("httpContext"); } IPrincipal user = httpContext.User; if (!user.Identity.IsAuthenticated) { return false; } if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase)) { return false; } if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole)) { return false; } return true;}
上述原始碼十分清晰的說明了問題。另外,要實現角色驗證,需要在web.config中配置一個roleManager,通常可以使用SqlRoleProvider,也可以自訂。
如果考慮到Output Caching,Authorization Filters有什麼tricky嗎?我們看到OnAuthorization中的一段代碼:
if (AuthorizeCore(filterContext.HttpContext)) { // ** IMPORTANT ** // Since we're performing authorization at the action level, the authorization code runs // after the output caching module. In the worst case this could allow an authorized user // to cause the page to be cached, then an unauthorized user would later be served the // cached page. We work around this by telling proxies not to cache the sensitive page, // then we hook our custom authorization code into the caching mechanism so that we have // the final say on whether a page should be served from the cache. HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache; cachePolicy.SetProxyMaxAge(new TimeSpan(0)); cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */); }
這段代碼本沒什麼出奇的地方,先是調用AuthorizeCore,接著看到這段注釋,大意是:我們把驗證放到了Action部分,使得驗證代碼將在緩衝模組後面執行。考慮到最壞的情況,一個認證的使用者得到了一個敏感的頁面,並且這個頁面被緩衝了,接著,一個未驗證的使用者將得到快取頁面面而不需驗證。我們繞過了這個問題,直接告訴代理不要緩衝敏感頁面,並且把我們的驗證機制注入到緩衝機制中,使我們最終決定是否返回快取頁面面。
這段注釋清楚的說明了驗證機制和緩衝機制的矛盾,也給出瞭解決方案,因此,如果要自己實現IAuthorizationFilter一定要小心了,一定盡量繼承AuthorizeAttribute並僅重寫AuthorizeCore。如果還不明白看看下面回呼函數CacheValidateHandler最終調用的OnCacheAuthorization的實現就知道了:
protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext) { if (httpContext == null) { throw new ArgumentNullException("httpContext"); } bool isAuthorized = AuthorizeCore(httpContext); return (isAuthorized) ? HttpValidationStatus.Valid : HttpValidationStatus.IgnoreThisRequest; }
在輸出緩衝之前仍然先調用AuthorizeCore,這樣就避免了上面注釋中提到的問題。
如果AuthorizeAttribute認證失敗,將構造一個HttpUnauthorizedResult,並附給filterContext.Result。HttpUnauthorizedResult同樣繼承自ActionResult。其ExecuteResult方法如下:
context.HttpContext.Response.StatusCode = 401;
簡單的返回一個401錯誤,表示未驗證錯誤,然後驗證模組根據web.config的配置進行下一步操作,通常是跳轉到一個登入頁面。如果不希望這樣,可以重寫AuthorizeAttribute的HandleUnauthorizedRequest方法。比如在一個Ajax請求因為驗證錯誤而拒絕,你顯然不希望頁面跳轉。可以像下面這樣處理:
protected override void HandleUnauthorizedRequest(AuthorizationContext context){if (context.HttpContext.Request.IsAjaxRequest()) {UrlHelper urlHelper = new UrlHelper(context.RequestContext);context.Result = new JsonResult {Data = new {Error = "NotAuthorized",LogOnUrl = urlHelper.Action("LogOn", "Account")},JsonRequestBehavior = JsonRequestBehavior.AllowGet};}elsebase.HandleUnauthorizedRequest(context);}
IExceptionFilter
從前面一節的虛擬碼中可以看到,IExceptionFilter被設計成能夠捕獲異常。然而需要注意:僅僅從action開始執行之後的異常可以用這種方式捕獲(包括過濾器執行期間),在這之前的,諸如找不到controller,找不到action之類的異常是無法用IExceptionFilter捕獲的。
MVC內建了一個HandleErrorAttribute,它的作用是在捕獲異常後注入500錯誤(對於404錯誤將不處理)。來看看其內部對filterContext.Result的處理:
filterContext.Result = new ViewResult {ViewName = View,MasterName = Master,ViewData = new ViewDataDictionary<HandleErrorInfo>(model),TempData = filterContext.Controller.TempData};
注意到使用者指定的View、Master會被返回,還會有一個HandleErrorInfo的Model被封裝成ViewData返回,還會附帶上當前Controller的TempData。HandleErrorInfo封裝了Exception對象,Controller和Action的名字。這些資訊可以在我們的錯誤頁面中使用。filterContext.Result會被MVC架構執行,所以我們可以用一個非ViewResult指定,比如RedirectToRouteResult。
ControllerActionInvoker在執行filterContext.Result之前會判斷一下filterContext.ExceptionHandled是否為true,如果不為true,filterContext.Result將不會執行,那麼該死的黃頁還是會拋向ASP.NET。HandleErrorAttribute將檢查ExceptionHandled,如果為true則什麼都不做返回,否則將ExceptionHandled置為true。當我們需要自己實現IExceptionFilter,在同時有多個IExceptionFilter的時候,可以通過ExceptionHandled通知後面執行的IExceptionFilter異常是否被處理了。還需要注意的是:上面提到IActionFilter 也可以處理異常,可以猜到ActionExecutedContext和ResultExecutedContext也具有ExceptionHandled,對應的,如果在OnActionExecuted和OnResultExecuted中將ExceptionHandled設成了true,MVC架構將不會重新拋出異常,於是任何一個IExceptionFilter將沒有機會執行。
Controller自身實現過濾
Controller自身繼承自上述的四個介面,並且允許其繼承類覆蓋實現,所以我們也可以通過重寫OnActionExecuting等方法,為Controller設定過濾,這種過濾將優先於用屬性的方式設定的過濾執行。
public abstract class Controller : IActionFilter, IAuthorizationFilter, IExceptionFilter, IResultFilter
勞動果實,轉載請註明出處:http://www.cnblogs.com/P_Chou/archive/2010/12/07/details-asp-net-mvc-08.html