第十五节: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 }
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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。