對InvokeAction簡略分析瞭解驗證失敗為什麼Action還會繼續執行
一、前言
有些同學使用AuthorizationFilter來進行使用者是否登入驗證,如果未登入就跳到登入頁。
很簡單的一個場景,但是有些同學會發現雖然驗證失敗了,但是整個Action還會執行一遍。
於是google啊google啊,然後找到了解決方案,但是不知為啥,下面就對InvokeAction的簡略分析,說說到底是為啥。
二、分析InvokeAction執行順序
1、首先獲取Controller和Action描述類
ControllerDescriptor controllerDescriptor = GetControllerDescriptor(controllerContext); ActionDescriptor actionDescriptor= FindAction(controllerContext, controllerDescriptor, actionName);
2、大家都知道每個Action執行前會先執行一些Filters,所以首先需要獲取所有FilterScope為Action的Filters
FilterInfo filterInfo = GetFilters(controllerContext, actionDescriptor);
3、Filter的執行是有順序的,AuthenticationFilter最先執行,接下來就是執行AuthenticationFilter的程式碼
AuthenticationContext authenticationContext = InvokeAuthenticationFilters(controllerContext, filterInfo.AuthenticationFilters, actionDescriptor);
這裡要注意,AuthenticationContext有個屬性Result,如果驗證失敗一定要設定這個屬性的值,否則看看下面的程式碼就知道了。
if (authenticationContext.Result != null) { // An authentication filter signaled that we should short-circuit the request. Let all // authentication filters contribute to an action result (to combine authentication// challenges). Then, run this action result. AuthenticationChallengeContext challengeContext = InvokeAuthenticationFiltersChallenge( controllerContext, filterInfo.AuthenticationFilters, actionDescriptor, authenticationContext.Result); InvokeActionResult(controllerContext, challengeContext.Result ?? authenticationContext.Result); } else { ... }
看到了吧,如果你設定Result的值,此時就會執行你設定的Result,如果不設定就會繼續走下去,會走哪去,一直往後看,你就知道了。
插一下,我們先看看 InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult)是什麼東東。
protected virtual void InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult) { actionResult.ExecuteResult(controllerContext); }
這個方法看字面意思是執行一個ActionResult,還是看兩個簡單的例子吧。
(1)ContentResult
public override void ExecuteResult(ControllerContext context) { if (context == null) { throw new ArgumentNullException("context"); } HttpResponseBase response = context.HttpContext.Response; if (!String.IsNullOrEmpty(ContentType)) { response.ContentType = ContentType; } if (ContentEncoding != null) { response.ContentEncoding = ContentEncoding; } if (Content != null) { response.Write(Content); } }
很簡單吧,就是把你設定的Content輸出到頁面中,用的也是response.Write(content)方法,所以在Mvc中完全可以用ContentResult替換response.Write(content).
(2)HttpStatusCodeResult
public override void ExecuteResult(ControllerContext context) { if (context == null) { throw new ArgumentNullException("context"); } context.HttpContext.Response.StatusCode = StatusCode; if (StatusDescription != null) { context.HttpContext.Response.StatusDescription = StatusDescription; } }
HttpStatusCodeResult 是HttpUnauthorizedResult 的基類,HttpUnauthorizedResult是什麼?自己看字面意思也看出來了。
這個Result的ExecuteResult的更簡單,只是設定Response.StatusCode和StatusDescription。
4、回到InvokeAction中來,接著InvokeAuthenticationFilters這個方法的執行往下看
if (authenticationContext.Result != null)已經解釋了,然後看看else。else中就開始執行AuthorizationFilters了,此處終於知道為什麼AuthenticationFilters執行在前了吧。
只有authenticationContext.Result不設定值的時候,才會執行AuthorizationFilters,所以如果你想AuthenticationFilters驗證失敗之後,就退出Action,一定要設定Result.
其實AuthorizationFilters的執行方式和AuthenticationFilters類似,如果設定了AuthorizationContext的Result屬性,就會執行Result,否則就繼續往下執行。
AuthorizationContext authorizationContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor); if (authorizationContext.Result != null) { // An authorization filter signaled that we should short-circuit the request. Let all // authentication filters contribute to an action result (to combine authentication // challenges). Then, run this action result. AuthenticationChallengeContext challengeContext = InvokeAuthenticationFiltersChallenge( controllerContext, filterInfo.AuthenticationFilters, actionDescriptor, authorizationContext.Result); InvokeActionResult(controllerContext, challengeContext.Result ?? authorizationContext.Result); } else { // 如果你沒有其他的Filters、沒有設定OnActionXXX等等,你的Action內的程式碼就會在這裡被呼叫執行了。 ... }
看到這裡,是不是有些同學會想:“原來如此啊”。
三、總結
由於微軟沒有提供AuthenticationFilter對應的基類,只提供了一個IAuthenticationFilter,所以我們就用AuthorizeAttribute來進行總結。
一些同學自定義AuthorizationFilters的時候,程式碼結尾沒有呼叫基類的 OnAuthorization,也沒有設定FilterContext的Result屬性,只是進行Response.Redirect或者其他認為跳出Action執行的程式碼。這樣其實等於沒有進行驗證,Action中的程式碼還是會執行。
在進行AuthorizationFilters的時候,如果想驗證失敗之後,不執行Action的方法,可以有以下兩種方法:
(1)呼叫基類的 OnAuthorization,我們看看基類 OnAuthorization 都幹了啥?
public virtual void OnAuthorization(AuthorizationContext filterContext) { ... if (AuthorizeCore(filterContext.HttpContext)) { ... } else { HandleUnauthorizedRequest(filterContext); // 主要看這個方法 } } protected virtual void HandleUnauthorizedRequest(AuthorizationContext filterContext) { // Returns HTTP 401 - see comment in HttpUnauthorizedResult.cs. filterContext.Result = new HttpUnauthorizedResult(); }
HttpUnauthorizedResult:這個類有沒有看著眼熟,對了,就是前面講Result中的第二個例子的子類。
(2)給filterContext的Result屬性設定一個Result,即使是EmptyResult都可以,如果你想輸出點內容到頁面中,那就使用ContetResult,前面說過它是通過呼叫Response.Write()來實現的。
最後附上ControllerActionInvoker:InvokeAction(L230)方法的詳細內容:
public virtual bool InvokeAction(ControllerContext controllerContext, string actionName) { if (controllerContext == null) { throw new ArgumentNullException("controllerContext"); } Contract.Assert(controllerContext.RouteData != null); if (String.IsNullOrEmpty(actionName) && !controllerContext.RouteData.HasDirectRouteMatch()) { throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName"); } ControllerDescriptor controllerDescriptor = GetControllerDescriptor(controllerContext); ActionDescriptor actionDescriptor = FindAction(controllerContext, controllerDescriptor, actionName); if (actionDescriptor != null) { FilterInfo filterInfo = GetFilters(controllerContext, actionDescriptor); try { AuthenticationContext authenticationContext = InvokeAuthenticationFilters(controllerContext, filterInfo.AuthenticationFilters, actionDescriptor); if (authenticationContext.Result != null) { // An authentication filter signaled that we should short-circuit the request. Let all // authentication filters contribute to an action result (to combine authentication // challenges). Then, run this action result. AuthenticationChallengeContext challengeContext = InvokeAuthenticationFiltersChallenge( controllerContext, filterInfo.AuthenticationFilters, actionDescriptor, authenticationContext.Result); InvokeActionResult(controllerContext, challengeContext.Result ?? authenticationContext.Result); } else { AuthorizationContext authorizationContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor); if (authorizationContext.Result != null) { // An authorization filter signaled that we should short-circuit the request. Let all // authentication filters contribute to an action result (to combine authentication // challenges). Then, run this action result. AuthenticationChallengeContext challengeContext = InvokeAuthenticationFiltersChallenge( controllerContext, filterInfo.AuthenticationFilters, actionDescriptor, authorizationContext.Result); InvokeActionResult(controllerContext, challengeContext.Result ?? authorizationContext.Result); } else { if (controllerContext.Controller.ValidateRequest) { ValidateRequest(controllerContext); } IDictionary<string, object> parameters = GetParameterValues(controllerContext, actionDescriptor); ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters); // The action succeeded. Let all authentication filters contribute to an action result (to // combine authentication challenges; some authentication filters need to do negotiation // even on a successful result). Then, run this action result. AuthenticationChallengeContext challengeContext = InvokeAuthenticationFiltersChallenge( controllerContext, filterInfo.AuthenticationFilters, actionDescriptor, postActionContext.Result); InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters, challengeContext.Result ?? postActionContext.Result); } } } 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. throw; } catch (Exception ex) { // something blew up, so execute the exception filters ExceptionContext exceptionContext = InvokeExceptionFilters(controllerContext, filterInfo.ExceptionFilters, ex); if (!exceptionContext.ExceptionHandled) { throw; } InvokeActionResult(controllerContext, exceptionContext.Result); } return true; } // notify controller that no method matched return false; }View Code