在webform中,驗證的流程大致如下圖: 在AOP中: 在Filter中: AuthorizeAttribute許可權驗證 登錄後有許可權控制,有的頁面是需要用戶登錄才能訪問的,需要在訪問頁面增加一個驗證,也不能每個action都一遍。 1、寫一個CustomAuthorAttribute,繼承自Au ...
在webform中,驗證的流程大致如下圖:
在AOP中:
在Filter中:
AuthorizeAttribute許可權驗證
登錄後有許可權控制,有的頁面是需要用戶登錄才能訪問的,需要在訪問頁面增加一個驗證,也不能每個action都一遍。
1、寫一個CustomAuthorAttribute,繼承自AuthorizeAttribute,重寫OnAuthorization方法,在裡面把邏輯寫成自己的。
2、有方法註冊和控制器註冊。
3、有全局註冊,全部控制器全部action都生效。
但是在這個裡面,首先要驗證登錄首頁,首頁沒有鄧麗,就跑到登錄頁面了,但是登錄頁面也要走特性裡面的邏輯,又重定向到鄧麗。。。迴圈了。。。。
這裡有一個AlloAnonymous,這個標簽就可以解決這個迴圈的問題,匿名支持,不需要登錄就可以,但是單單加特性是沒有用的,其實需要驗證時支持,甚至可以說自己自定義一個特性也是可以的,這個特性裡面是空的,只是為了用來做標記。
特性的使用範圍,希望特性通用,在不同的系統,不同的地址登錄,==》在特性上面加個傳參的構造函數。
public class CustomAllowAnonymousAttribute : Attribute { }
CustomAuthorAttribute類
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class CustomAuthorizeAttribute : AuthorizeAttribute { private Logger logger = new Logger(typeof(CustomAuthorizeAttribute)); private string _LoginUrl = null; public CustomAuthorizeAttribute(string loginUrl = "~/Home/Login") { this._LoginUrl = loginUrl; } //public CustomAuthorizeAttribute(ICompanyUserService service) //{ //} //不行 public override void OnAuthorization(AuthorizationContext filterContext) { var httpContext = filterContext.HttpContext;//能拿到httpcontext 就可以為所欲為 if (filterContext.ActionDescriptor.IsDefined(typeof(CustomAllowAnonymousAttribute), true)) { return; } else if (filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(CustomAllowAnonymousAttribute), true)) { return; } else if (httpContext.Session["CurrentUser"] == null || !(httpContext.Session["CurrentUser"] is CurrentUser))//為空了, { //這裡有用戶,有地址 其實可以檢查許可權 if (httpContext.Request.IsAjaxRequest()) //httpContext.Request.Headers["xxx"].Equals("XMLHttpRequst") { filterContext.Result = new NewtonJsonResult( new AjaxResult() { Result = DoResult.OverTime, DebugMessage = "登陸過期", RetValue = "" }); } else { httpContext.Session["CurrentUrl"] = httpContext.Request.Url.AbsoluteUri; filterContext.Result = new RedirectResult(this._LoginUrl); //短路器:指定了Result,那麼請求就截止了,不會執行action } } else { CurrentUser user = (CurrentUser)httpContext.Session["CurrentUser"]; //this.logger.Info($"{user.Name}登陸了系統"); return;//繼續 } //base.OnAuthorization(filterContext); } }
Filter生效機制
為什麼加個標簽,繼承AuthorizeAttribute,重寫OnAuthorization方法就可以了呢?控制器已經實例化,調用ExecuteCore方法,找到方法名字,ControllerActionInvokee.InvokeAction,找到全部的Filter特性,InvokeAuthorize--result不為空,直接InvokeActionResult,為空就正常執行Action。
有一個實例類型,有一個方法名稱,希望你反射執行
在找到方法後,執行方法前,可以檢測下特性,來自全局的、來自控制器的、來自方法的。價差特性,特性是自己預定義的,按類執行,定個標識,為空就正常,不為空就跳轉,正常就繼續執行。
Filter原理和AOP面向切麵編程
Filter是AOP思想的一種實現,其實就是ControllerActionInvoke這個類中,有個InvokeAction方法,控制器實例化之後,ActionInvoke前後,通過檢測預定義Filter並且執行它,達到AOP的目的。
下麵是InvokeAction的源碼:
public virtual bool InvokeAction(ControllerContext controllerContext, string actionName) { if (controllerContext == null) { throw new ArgumentNullException("controllerContext"); } if (string.IsNullOrEmpty(actionName) && !controllerContext.RouteData.HasDirectRouteMatch()) { throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName"); } ControllerDescriptor controllerDescriptor = this.GetControllerDescriptor(controllerContext); ActionDescriptor actionDescriptor = this.FindAction(controllerContext, controllerDescriptor, actionName); if (actionDescriptor != null) { FilterInfo filters = this.GetFilters(controllerContext, actionDescriptor); try { AuthenticationContext authenticationContext = this.InvokeAuthenticationFilters(controllerContext, filters.AuthenticationFilters, actionDescriptor); if (authenticationContext.Result != null) { AuthenticationChallengeContext authenticationChallengeContext = this.InvokeAuthenticationFiltersChallenge(controllerContext, filters.AuthenticationFilters, actionDescriptor, authenticationContext.Result); this.InvokeActionResult(controllerContext, authenticationChallengeContext.Result ?? authenticationContext.Result); } else { AuthorizationContext authorizationContext = this.InvokeAuthorizationFilters(controllerContext, filters.AuthorizationFilters, actionDescriptor); if (authorizationContext.Result != null) { AuthenticationChallengeContext authenticationChallengeContext2 = this.InvokeAuthenticationFiltersChallenge(controllerContext, filters.AuthenticationFilters, actionDescriptor, authorizationContext.Result); this.InvokeActionResult(controllerContext, authenticationChallengeContext2.Result ?? authorizationContext.Result); } else { if (controllerContext.Controller.ValidateRequest) { ControllerActionInvoker.ValidateRequest(controllerContext); } IDictionary<string, object> parameterValues = this.GetParameterValues(controllerContext, actionDescriptor); ActionExecutedContext actionExecutedContext = this.InvokeActionMethodWithFilters(controllerContext, filters.ActionFilters, actionDescriptor, parameterValues); AuthenticationChallengeContext authenticationChallengeContext3 = this.InvokeAuthenticationFiltersChallenge(controllerContext, filters.AuthenticationFilters, actionDescriptor, actionExecutedContext.Result); this.InvokeActionResultWithFilters(controllerContext, filters.ResultFilters, authenticationChallengeContext3.Result ?? actionExecutedContext.Result); } } } catch (ThreadAbortException) { throw; } catch (Exception exception) { ExceptionContext exceptionContext = this.InvokeExceptionFilters(controllerContext, filters.ExceptionFilters, exception); if (!exceptionContext.ExceptionHandled) { throw; } this.InvokeActionResult(controllerContext, exceptionContext.Result); } return true; } return false; }View Code
全局異常處理HandleErrorAttribute
關於異常處理的建議:
1、避免UI層直接看到異常,每個控制器裡面try-catch一下?不是很麻煩嗎?
2、這個時候,AOP就登場了,HandleErrorAttribute,自己寫一個特性,繼承之HandleErrorAttribute,重寫OnException,在發生異常之後,會跳轉到這個方法。
在這邊,一定要
public class CustomHandleErrorAttribute : HandleErrorAttribute { private Logger logger = new Logger(typeof(CustomHandleErrorAttribute)); /// <summary> /// 會在異常發生後,跳轉到這個方法 /// </summary> /// <param name="filterContext"></param> public override void OnException(ExceptionContext filterContext) { var httpContext = filterContext.HttpContext;//"為所欲為" if (!filterContext.ExceptionHandled)//沒有被別的HandleErrorAttribute處理 { this.logger.Error($"在響應 {httpContext.Request.Url.AbsoluteUri} 時出現異常,信息:{filterContext.Exception.Message}");// if (httpContext.Request.IsAjaxRequest()) { filterContext.Result = new NewtonJsonResult( new AjaxResult() { Result = DoResult.Failed, DebugMessage = filterContext.Exception.Message, RetValue = "", PromptMsg = "發生錯誤,請聯繫管理員" }); } else { filterContext.Result = new ViewResult()//短路器 { ViewName = "~/Views/Shared/Error.cshtml", ViewData = new ViewDataDictionary<string>(filterContext.Exception.Message) }; } filterContext.ExceptionHandled = true;//已經被我處理了 } } }
這個是要重新跳轉的地址:
一定要考慮到是不是Ajax請求的
多種異常情況,能不能進入自定義的異常呢?
1、Action異常,沒有被Catch
2、Action異常,被Catch
3、Action調用Service異常
4、Action正常視圖出現異常了
5、控制器構造出現異常
6、Action名稱錯誤
7、任意地址錯誤
8、許可權Filter異常
答案:
1、可以
2、不可以
3、可以,異常冒泡
4、可以,為什麼呢?因為ExecuteResult是包裹在try裡面的
5、不可以的,Filter是在構造完成控制之後方法執行之前完成的
6、不可以的,因為請求都沒進MVC流程
7、不可以的,因為請求都沒進MVC
8、可以的,許可權Filter也是在try裡面的。
那這些沒有被捕獲的異常怎麼辦?還有一個方法
在Global中增加一個事件
public class MvcApplication : System.Web.HttpApplication { private Logger logger = new Logger(typeof(MvcApplication)); protected void Application_Start() { AreaRegistration.RegisterAllAreas();//註冊區域 FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);//註冊全局的Filter RouteConfig.RegisterRoutes(RouteTable.Routes);//註冊路由 BundleConfig.RegisterBundles(BundleTable.Bundles);//合併壓縮 ,打包工具 Combres ControllerBuilder.Current.SetControllerFactory(new ElevenControllerFactory()); this.logger.Info("網站啟動了。。。"); } /// <summary> /// 全局式的異常處理,可以抓住漏網之魚 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> protected void Application_Error(object sender, EventArgs e) { Exception excetion = Server.GetLastError(); this.logger.Error($"{base.Context.Request.Url.AbsoluteUri}出現異常"); Response.Write("System is Error...."); Server.ClearError(); //Response.Redirect //base.Context.RewritePath("/Home/Error?msg=") }
HandleErrorAttribute+Application_Error,粒度不一樣,能拿到的東西不一樣
IActionFilter擴展定製
IActionFilter
1、OnActionExecuting 方法執行前
2、OnActionExecuted方法執行後
3、OnResultExecuting結果執行前
4、OnResultExecuted結果執行後
先執行許可權Filter,再執行ActionFilter。
執行的順序:
Global OnActionExecuting
Controller OnActionExecuting
Action OnActionExecuting
Action真實執行
Action OnActionExecuted
Controller OnActionExecuted
Global OnActionExecuted
不同位置註冊的生效順序:全局---》控制器-----》Action
好像一個俄羅斯套娃,或者說洋蔥模型
在同一個位置註冊的生效順序,同一個位置按照先後順序生效,還有一個Order的參數,不設置Order預設是1,設置之後按照從小到大執行
ActionFilter能幹什麼?
日誌、參數檢測、緩存、重寫視圖、壓縮、防盜鏈、統計訪問、不同的客戶端跳轉不同的頁面、限流.....
瀏覽器請求時,會聲明支持的格式,預設的IIS是沒有壓縮的,檢測了支持的格式,在響應時將數據壓縮(IIS伺服器完成的),在響應頭裡面加上Content-Encoding,瀏覽器查看數據格式,按照瀏覽器格式解壓(無論你是什麼東西,都可以壓縮解壓的),壓縮是IIS,解壓是瀏覽器的。
public class CompressActionFilterAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { //foreach (var item in filterContext.ActionParameters) //{ // //參數檢測 敏感詞過濾 //} var request = filterContext.HttpContext.Request; var respose = filterContext.HttpContext.Response; string acceptEncoding = request.Headers["Accept-Encoding"];//檢測支持格式 if (!string.IsNullOrWhiteSpace(acceptEncoding) && acceptEncoding.ToUpper().Contains("GZIP")) { respose.AddHeader("Content-Encoding", "gzip");//響應頭指定類型 respose.Filter = new GZipStream(respose.Filter, CompressionMode.Compress);//壓縮類型指定 } } } public class LimitActionFilterAttribute : ActionFilterAttribute { private int _Max = 0; public LimitActionFilterAttribute(int max = 1000) { this._Max = max; } public override void OnActionExecuting(ActionExecutingContext filterContext) { string key = $"{filterContext.RouteData.Values["Controller"]}_{filterContext.RouteData.Values["Action"]}"; //CacheManager.Add(key,) 存到緩存key 集合 時間 filterContext.Result = new JsonResult() { Data = new { Msg = "超出頻率" } }; } }
Filter這麼厲害,有沒有什麼局限性????
雖然很豐富,但是只能以Action為單位,Action內部調用別的類庫,加操作就做不到!這種就得靠IOC+AOP擴展。
本篇只是介紹了.NET Framework MVC 中的過濾器Filter(許可權特性、Action、Result、Exception),其實在.NET Core MVC 增加了ResourceFilter,加了這個特性,資源特性,Action/Result /Exception三個特性沒有什麼變化。後面記錄到到.NET Core MVC時再詳細介紹。