一. 簡介
1. 說明
提到過濾器,通常是指請求處理管道中特定階段之前或之后的代碼,可以處理:授權、響應緩存(對請求管道進行短路,以便返回緩存的響應)、 防盜鏈、本地化國際化等,過濾器用於橫向處理業務,符合Aop思想,它也可以有效的避免代碼的重復復制。
在Asp.Net Core中,有5種過濾器,分別是授權、資源、操作、結果、異常五大過濾器,與之前的Asp.Net 相比,多了一個資源過濾器,剩下的4個授權、 操作、結果、異常過濾器則沒有什么太大的區別。
PS: 傳統Asp.Net 中的4種過濾器參考 https://www.cnblogs.com/yaopengfei/p/7910763.html
2. 獲取區域、控制器、Action的名稱
(1). 方法1
context.ActionDescriptor.RouteValues["area"].ToString();
context.ActionDescriptor.RouteValues["controller"].ToString();
context.ActionDescriptor.RouteValues["action"].ToString();
(2). 方法2:
context.RouteData.Values["controller"].ToString();
context.RouteData.Values["action"].ToString();
測試案例詳見“AuthorizeFilter”類,以特性的形式作用於Areas下的TestController下的Index。
代碼如下:
1 public class AuthorizeFilter : Attribute,IAuthorizationFilter 2 { 3 public void OnAuthorization(AuthorizationFilterContext context) 4 { 5 //1. 獲取區域、控制器、Action的名稱 6 //必須在區域里的控制器上加個特性[Area("")]才能獲取 7 var areaName = context.ActionDescriptor.RouteValues["area"] == null ? "" : context.ActionDescriptor.RouteValues["area"].ToString(); 8 var controllerName = context.ActionDescriptor.RouteValues["controller"] == null ? "" : context.ActionDescriptor.RouteValues["controller"].ToString(); 9 var actionName = context.ActionDescriptor.RouteValues["action"] == null ? "" : context.ActionDescriptor.RouteValues["action"].ToString(); 10 11 //下面的方式也能獲取控制器和action的名稱 12 //var controllerName = context.RouteData.Values["controller"].ToString(); 13 //var actionName = context.RouteData.Values["action"].ToString(); 14 15 } 16 17 }
特別注意:如果要獲取Areas名稱,必須在區域里的控制器上加個特性[Area("")]才能獲取,這一點和以前的Asp.Net不同。
3. 作用域:同傳統Asp.Net相同,可以作用於全局、控制器、Action。
(1).情況一: 過濾器中沒有構造函數,如:AuthorizeFilter類
A.作用於全局:在ConfigureService中的AddMvc方法中進行注入,有兩種寫法,如:o.Filters.Add(typeof(AuthorizeFilter)); 或 o.Filters.Add(new AuthorizeFilter());
B.作用於Controller或Action: 直接以特性的形式作用於Controller或Action即可,與Asp.Net中相同。
(2).情況二: 過濾器中有構造函數,且構造函數中注入了其他類型,如:AuthorizeFilter2 類
分享AuthorizeFilter2代碼:

1 /// <summary> 2 /// 授權過濾器2(含構造函數) 3 /// </summary> 4 public class AuthorizeFilter2 : Attribute, IAuthorizationFilter 5 { 6 private IConfiguration Configuration; 7 8 public AuthorizeFilter2(IConfiguration configuration) 9 { 10 Configuration = configuration; 11 } 12 13 public void OnAuthorization(AuthorizationFilterContext context) 14 { 15 //1. 獲取區域、控制器、Action的名稱 16 //必須在區域里的控制器上加個特性[Area("")]才能獲取 17 var areaName = context.ActionDescriptor.RouteValues["area"] == null ? "" : context.ActionDescriptor.RouteValues["area"].ToString(); 18 var controllerName = context.ActionDescriptor.RouteValues["controller"] == null ? "" : context.ActionDescriptor.RouteValues["controller"].ToString(); 19 var actionName = context.ActionDescriptor.RouteValues["action"] == null ? "" : context.ActionDescriptor.RouteValues["action"].ToString(); 20 21 //2. 測試構造函數注入內容的讀取 22 var myName = Configuration["myName"]; 23 } 24 }
A.作用於全局:與上面情況一用法一樣,直接在AddMvc方法中進行注入,即可以使用過濾器中的構造函數中注入的對象,不需要特殊處理。
B.作用於Controller或Action:發現如果直接以特性的形式進行作用,會報錯缺少參數,這個時候正式引入兩個特別的內置類,來處理這個問題:
① ServiceFilterAttribute:首先在控制器或action上這樣用 [ServiceFilter(typeof(AuthorizeFilter2))], 然后在 ConfigureService中對該類進行注冊一下, 如: services.AddScoped<AuthorizeFilter2>();
② TypeFilterAttribute: 在控制器或action上這樣用 [TypeFilter(typeof(AuthorizeFilter2))] 即可,如下面的Index,不需要再在ConfigureService中進行注冊了, 相比上面的ServiceFilterAttribute更方便。
代碼見上面
③ 在屬性上實現 IFilterFactory:通過繼承TypeFilterAttribute來實現,TypeFilterAttribute 可實現 IFilterFactory。 IFilterFactory
公開用於創建 IFilterMetadata 實例的 CreateInstance 方法,CreateInstance
從服務容器 (DI) 中加載指定的類型。
代碼如下:
1 /// <summary> 2 /// 授權過濾器3(含構造函數 在屬性上實現IFilterFactory) 3 /// </summary> 4 public class AuthorizeFilter3 : TypeFilterAttribute 5 { 6 public AuthorizeFilter3() : base(typeof(AuthorizeFilter3Impl)) 7 { 8 } 9 10 private class AuthorizeFilter3Impl : IAuthorizationFilter 11 { 12 private IConfiguration Configuration; 13 public AuthorizeFilter3Impl(IConfiguration configuration) 14 { 15 Configuration = configuration; 16 } 17 18 public void OnAuthorization(AuthorizationFilterContext context) 19 { 20 //1. 獲取區域、控制器、Action的名稱 21 //必須在區域里的控制器上加個特性[Area("")]才能獲取 22 var areaName = context.ActionDescriptor.RouteValues["area"] == null ? "" : context.ActionDescriptor.RouteValues["area"].ToString(); 23 var controllerName = context.ActionDescriptor.RouteValues["controller"] == null ? "" : context.ActionDescriptor.RouteValues["controller"].ToString(); 24 var actionName = context.ActionDescriptor.RouteValues["action"] == null ? "" : context.ActionDescriptor.RouteValues["action"].ToString(); 25 26 //2. 測試構造函數注入內容的讀取 27 var myName = Configuration["myName"]; 28 } 29 } 30 }
4. 取消和設置短路
(1).過濾器直接取消:通過context.Result來截斷請求,使過濾器管道短路
(2).頁面的跳轉:需要區分是否是ajax請求,然后通過上面的context.Result返回不同的內容。
PS:根據request.Headers["X-Requested-With"]是否包含XMLHttpRequest來判斷是不是ajax請求。
1 /// <summary> 2 /// 授權過濾器 3 /// </summary> 4 public class AuthorizeFilter : Attribute,IAuthorizationFilter 5 { 6 public void OnAuthorization(AuthorizationFilterContext context) 7 { 8 //1. 獲取區域、控制器、Action的名稱 9 //必須在區域里的控制器上加個特性[Area("")]才能獲取 10 var areaName = context.ActionDescriptor.RouteValues["area"] == null ? "" : context.ActionDescriptor.RouteValues["area"].ToString(); 11 var controllerName = context.ActionDescriptor.RouteValues["controller"] == null ? "" : context.ActionDescriptor.RouteValues["controller"].ToString(); 12 var actionName = context.ActionDescriptor.RouteValues["action"] == null ? "" : context.ActionDescriptor.RouteValues["action"].ToString(); 13 14 //下面的方式也能獲取控制器和action的名稱 15 //var controllerName = context.RouteData.Values["controller"].ToString(); 16 //var actionName = context.RouteData.Values["action"].ToString(); 17 18 //2.判斷是什么請求,進行響應的頁面跳轉 19 if (IsAjaxRequest(context.HttpContext.Request)) 20 { 21 //2.1 是ajax請求 22 context.Result = new JsonResult(new 23 { 24 status = "error", 25 message = "您沒有權限" 26 }); 27 } 28 else 29 { 30 //2.2 不是ajax請求 31 var result = new ViewResult { ViewName = "~/Views/Shared/Error.cshtml" }; 32 context.Result = result; 33 } 34 } 35 36 /// <summary> 37 /// 判斷該請求是否是ajax請求 38 /// </summary> 39 /// <param name="request"></param> 40 /// <returns></returns> 41 private bool IsAjaxRequest(HttpRequest request) 42 { 43 string header = request.Headers["X-Requested-With"]; 44 return "XMLHttpRequest".Equals(header); 45 } 46 }
二. 五大過濾器
補充一下內置的過濾器(此處建個表格說明一下繼承類或接口)
(1).ActionFilterAttribute
(2).ExceptionFilterAttribute
(3).ResultFilterAttribute
(4).FormatFilterAttribute
(5).ServiceFilterAttribute:用於處理含構造函數的自定義過濾器,但需要先注冊。
(6).TypeFilterAttribute:用於處理含構造函數的自定義過濾器,不需要注冊。
1.授權過濾器
(1) 說明:它是過濾器管道中第一個過濾器,控制對方法的訪問,僅有在它之前執行的方法,沒有之后;在授權過濾器中不會處理異常, 異常過濾器也捕獲到其中產生的異常,因此要小心應對。
(2) 實現:繼承Attribute類,實現IAuthorizationFilter接口,重寫OnAuthorization方法。
注:繼承Attribute類的目的是可以該過濾器以特性的形式作用於Controller或Action,下面過濾器都類似,不再說明。
(3).用途:通常用來做權限校驗(詳見下面案例應用)。
2. 資源過濾器
(1) 說明:只有授權過濾器在資源過濾器之前運行,里面的OnResourceExecuting重寫是在創建控制器調用的。
(2) 實現:繼承Attribute類,實現IResourceFilter接口,重寫OnResourceExecuting 和 OnResourceExecuted方法。
(異步的話實現IAsyncResourceFilter接口,重寫OnResourceExecutionAsync方法)
(3) 用途:做一些對變化要求不高的頁面的緩存(詳見下面案例應用)。
3. 操作過濾器(行為過濾器)
(1) 說明:分別在操作方法之前和之后執行
(2) 實現:繼承Attribute類,實現IActionFilter接口,重寫OnActionExecuting 和 OnActionExecuted方法。 或者直接繼承ActionFilterAttribute類,觀察源碼可知,該類繼承了Attribute類,而且還實現IActionFilter,IResultFilter接口。(異步的話實現IAsyncActionFilter接口,重寫OnActionExecutionAsync方法)
4. 結果過濾器
(1) 說明:在方法執行前后,且操作過濾器之后;結果(如:頁面渲染)的前后運行。
(2) 實現:繼承Attribute類,實現IResultFilter接口,重寫OnResultExecuting 和 OnResultExecuted方法。 或者直接繼承ResultFilterAttribute類,(或ActionFilterAttribute類), 觀察源碼可知,該類繼承了Attribute類,而且還實現IResultFilter接口。(異步的話實現IAsyncActionFilter接口, 重寫OnActionExecutionAsync方法) 還可以實現:IAlwaysRunResultFilter 或 IAsyncAlwaysRunResultFilter 接口。
(3).用途:可以獲取action的返回結果,進行一些處理,比如:根據要求返回json數據或jsonp數據(詳見cors章節)。
5. 異常過濾器
(1) 說明:用於實現常見的錯誤處理策略,沒有之前和之后事件,處理 Razor 頁面或控制器創建、模型綁定、操作過濾器或操作方法中發生的未經處理的異常。 但無法捕獲資源過濾器、結果過濾器或 MVC 結果執行中發生的異常 。
(2) 實現:繼承Attribute類,實現IExceptionFilter接口,重寫OnException方法。 或者直接繼承ExceptionFilterAttribute類,觀察源碼可知,該類繼承了Attribute類,而且還實現IExceptionFilter接口。(異步的話實現 IAsyncExceptionFilter接口,重寫OnExceptionAsync方法)
(3) 用途:全局捕獲異常,進行相關處理。
三. 高級
1. 過濾器執行順序
異常過濾器不參與測試,測試剩余四個過濾器的執行順序,將四個過濾器在下面Index2方法上,經斷點測試執行順序如下:
OnAuthorization→OnResourceExecuting→創建控制器→OnActionExecuting→執行action業務→OnActionExecuted→OnResultExecuting→頁面渲染加載→
OnResultExecuted→OnResourceExecuted
2. 相同類型過濾器不同作用域的執行順序
A. 操作過濾器
經測試,將操作過濾器分別作用在 action、Controller、全局,通過加斷點測試執行順序如下:
OnActionExecuting(全局)→OnActionExecuting(Controller)→OnActionExecuting(action)→OnActionExecuted(action)→OnActionExecuted(Controller)→OnActionExecuted(全局)
那么原理是什么呢?如何修改這個順序呢?
接口IOrderedFilter,有個order屬性,有小到大,訪問順序是有小到大,默認是0,實現的時候需要聲明一個order屬性,然后以特性作用於Action或Controller的時候聲明order的值,如: [ActionOrderFilterController(Order =1)] 、[ActionOrderFilter(Order =-1)]
代碼如下:

1 /// <summary> 2 /// 測試操作過濾器,自定義Order 3 /// (測試作用於Action) 4 /// </summary> 5 public class ActionOrderFilter : Attribute,IActionFilter, IOrderedFilter 6 { 7 public int Order { get; set; } 8 9 public void OnActionExecuted(ActionExecutedContext context) 10 { 11 12 } 13 14 public void OnActionExecuting(ActionExecutingContext context) 15 { 16 17 } 18 } 19 /// <summary> 20 /// 測試操作過濾器,自定義Order 21 /// (測試作用於Controller) 22 /// </summary> 23 public class ActionOrderFilterController : Attribute, IActionFilter, IOrderedFilter 24 { 25 public int Order { get; set; } 26 27 public void OnActionExecuted(ActionExecutedContext context) 28 { 29 30 } 31 32 public void OnActionExecuting(ActionExecutingContext context) 33 { 34 35 } 36 } 37 /// <summary> 38 /// 測試操作過濾器,自定義Order 39 /// (測試作用於全局) 40 /// </summary> 41 public class ActionOrderFilterGlobal : Attribute, IActionFilter, IOrderedFilter 42 { 43 public int Order { get; set; } 44 45 public void OnActionExecuted(ActionExecutedContext context) 46 { 47 48 } 49 50 public void OnActionExecuting(ActionExecutingContext context) 51 { 52 53 } 54 }
通過加斷點測試,執行順序如下:
OnActionExecuting(action)→OnActionExecuting(全局)→OnActionExecuting(Controller)→OnActionExecuted(Controller)→OnActionExecuted(全局)→OnActionExecuted(action)
B. 異常過濾器: action→controller→全局 (經過測試)
四. 案例應用
1. 做頁面緩存
(1).原理:資源過濾器中的OnResourceExecuting是在創建控制器之前執行的,我們可以截取地址頁面的地址作為緩存的key,然后判斷一下該key是否有值,有的話能否轉換成ViewResult,如果能,則直接context.Result截斷返回該頁面即可。
資源過濾器中的OnResourceExecuted在頁面渲染后執行,這個時候判斷一下上面的key是否有值,沒有的話將頁面ViewResult存到該key對應的緩存里。
代碼分享:
1 /// <summary> 2 /// 利用資源過濾器做靜態頁面緩存 3 /// 簡單版本實現,僅為了說明原理,並沒有做緩存過期等一系列操作 4 /// </summary> 5 public class MyPageCacheFilter : Attribute, IResourceFilter 6 { 7 private static readonly Dictionary<string, object> myCache = new Dictionary<string, object>(); 8 private string _cacheKey; 9 10 /// <summary> 11 /// 在創建控制器之前執行 12 /// </summary> 13 /// <param name="context"></param> 14 public void OnResourceExecuting(ResourceExecutingContext context) 15 { 16 _cacheKey = context.HttpContext.Request.Path.ToString(); 17 if (myCache.ContainsKey(_cacheKey)) 18 { 19 var cachedValue = myCache[_cacheKey] as ViewResult; 20 if (cachedValue != null) 21 { 22 context.Result = cachedValue;// 截斷請求 23 } 24 } 25 } 26 /// <summary> 27 /// 肯定在頁面渲染以后才執行了 28 /// </summary> 29 /// <param name="context"></param> 30 public void OnResourceExecuted(ResourceExecutedContext context) 31 { 32 if (!String.IsNullOrEmpty(_cacheKey) && !myCache.ContainsKey(_cacheKey)) 33 { 34 var result = context.Result as ViewResult; 35 if (result != null) 36 { 37 myCache.Add(_cacheKey, result); 38 } 39 } 40 } 41 }
(2).演示:過濾器代碼MyPageCacheFilter,測試頁面下面的Index4。
首先我們先進入到一個其他頁面,如:http://localhost:22164/Home/Index, 然后修改地址進入到 http://localhost:22164/Home/Index4 頁面,記錄下頁面的時間,刷新頁面,發現時間
不變,說明是從緩存中拿的頁面,而不是重新加載的。
1 /// <summary> 2 /// 測試利用資源過濾器做頁面緩存 3 /// </summary> 4 /// <returns></returns> 5 [MyPageCacheFilter] 6 public IActionResult Index4() 7 { 8 ViewBag.Time = DateTime.Now.ToString(); 9 return View(); 10 }
測試結果:
2. 做權限校驗
這里做一個簡單案例說明,首先聲明一個Skip特性,加在哪個action上面,表明該action不需要進行登錄校驗;而這里的登錄校驗只是簡單的判斷Session中是否有userName值,有的話校驗通過;沒有的話,寫入Session,並跳轉到錯誤頁面。
PS:Session的使用詳見Session章節,在過濾器中可以使用注入的方式進行Session的注入,也可以直接通過 context.httpcontext.session 來點出來。
下面是代碼分享:
特性的聲明
1 /// <summary> 2 /// 表示不需要校驗 3 /// </summary> 4 public class SkipAttribute:Attribute 5 { 6 }
校驗登錄的過濾器
1 /// <summary> 2 /// 校驗是否登錄 3 /// </summary> 4 public class CheckLogin : Attribute, IAuthorizationFilter 5 { 6 private readonly IHttpContextAccessor _httpContextAccessor; 7 private ISession _session => _httpContextAccessor.HttpContext.Session; 8 9 public CheckLogin(IHttpContextAccessor httpContextAccessor) 10 { 11 _httpContextAccessor = httpContextAccessor; 12 } 13 14 15 public void OnAuthorization(AuthorizationFilterContext context) 16 { 17 //也可以這樣獲取Session,就不需要注入了。 18 var testData = context.HttpContext.Session.GetString("userName"); 19 20 bool isHasAttr = false; 21 //所有目標對象上所有特性 22 var data = context.ActionDescriptor.EndpointMetadata.ToList(); 23 string attrName = typeof(SkipAttribute).ToString(); 24 //循環比對是否含有skip特性 25 for (int i = 0; i < data.Count; i++) 26 { 27 if (data[i].ToString().Equals(attrName)) 28 { 29 isHasAttr = true; 30 } 31 } 32 33 //1. 校驗是否標記跨過登錄驗證 34 if (isHasAttr) 35 { 36 //表示該方法或控制器跨過登錄驗證 37 //繼續走控制器中的業務即可 38 } 39 else 40 { 41 //這里只是簡單的做一下校驗 42 var userName = _session.GetString("userName"); 43 if (string.IsNullOrEmpty(userName)) 44 { 45 //表示沒有值,校驗沒有通過 46 _session.SetString("userName", "ypf"); 47 //截斷請求 48 context.Result=new RedirectResult("~/Views/Shared/Error.cshtml"); 49 } 50 } 51 } 52 }
作用的action
1 /// <summary> 2 /// 權限校驗 3 /// </summary> 4 /// <returns></returns> 5 //[Skip] 6 [TypeFilter(typeof(CheckLogin))] 7 public IActionResult Index5() 8 { 9 return View(); 10 }
補充一下Session注冊代碼
!
- 作 者 : Yaopengfei(姚鵬飛)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 聲 明1 : 本人才疏學淺,用郭德綱的話說“我是一個小學生”,如有錯誤,歡迎討論,請勿謾罵^_^。
- 聲 明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。