第十五節:Asp.Net Core中的各種過濾器(授權、資源、操作、結果、異常)


一. 簡介

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     }
View Code

 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     }
View Code

通過加斷點測試,執行順序如下:

  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 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM