ASP.NET MVC5基础-过滤器(Filters)详解

什么是过滤器?

通过上一篇关于Controller控制器的文章我们知道,MVC中的每一个请求,都会分配给相应的控制器(Controller)和对应的行为方法(Action)去处理,那么如果我们想要在Action处理的前后加上一些额外的处理逻辑怎么办呢?这时候就用到了过滤器(Filters)。

在ASP.NET MVC的请求处理过程中有19个管道事件,这些事件分布在请求处理的各个节点中,比如BeginRequest(开始处理请求时触发)、AuthenticateRequest(对请求进行身份验证时触发)、AuthorizeRequest(对请求进程授权时触发)...等等等等。而过滤器的主要作用就是将我们的附加逻辑注入到这些请求处理管道中。

在实际业务中,在Action方法前后添加额外附加逻辑的情况有很多,过滤器就是用来完成此功能。通过过滤器可以将与业务逻辑无关但经常需要执行的代码分离开,使我们的代码逻辑性更加清晰,代码更加简洁。

过滤器的类型与作用

MVC给我们提供了四种过滤器,基本满足了我们实际业务中常用的需求,包括以下:

过滤器类型名称 实现的接口 默认的实现类 作用 执行的顺序与节点
授权过滤器 IAuthorizationFilter AuthorizeAttribute 用于限制进入控制器或控制器的某个行为方法 在控制器方法调用前执行,所有过滤器中最先执行的
动作过滤器 IActionFilter ActionFilterAttribute 用于进入动作方法之前或之后的处理 在控制器方法调用前/后执行
结果过滤器 IResultFilter ActionFilterAttribute 用于动作方法返回结果之前或之后的处理 在控制器方法调用完,跳转至view页面前/后执行
异常处理过滤器 IExceptionFilter HandleErrorAttribute 用于处理某个动作方法或某个控制器里面抛出的异常 在控制器方法抛出异常时执行

这四种类型的接口是MVC对过滤器的一个接口规范,同时MVC默认通过AuthorizeAttribute(授权)、HandleErrorAttribute(异常处理)、ActionFilterAttribute(动作和结果)三个类实现了这四个接口。

需要注意的是ActionFilterAttribute类既实现了IActionFilter接口,也实现了IResultFilter接口。这是个抽象类,要求必须提供一个实现,AuthorizeAttribute和HandleErrorAttribute类则包含了一些有用的特性,可以不必创建派生类进行使用。所以我们一般都会通过继承ActionFilterAttribute类,实现自定义的过滤器。

除以上接口之外,我们还要用到FilterAttribute类,这个类将我们的过滤器包装成了特性,使我们的过滤器可以方便的在Action方法上方使用。

定义过滤器

过滤器有以下几个特点:

  • 可用于动作方法(Action)
  • 可用于控制器(Controller)
  • 可多个Filter可同时使用
  • 不同级别可以混搭
  • 可运用于基类的过滤器,会影响该基类的所有派生类

下面我们逐一介绍下基本过滤器的使用方法。

授权过滤器

所有实现了IAuthorizationFilter接口的都可以称之为授权过滤器。它的接口定义如下:

namespace System.Web.Mvc
{
    //
    // 摘要:
    //     定义授权筛选器所需的方法。
    public interface IAuthorizationFilter
    {
        //
        // 摘要:
        //     在需要授权时调用。
        //
        // 参数:
        //   filterContext:
        //     筛选器上下文。
        void OnAuthorization(AuthorizationContext filterContext);
    }
}

授权过滤器是最先运行的过滤器,它运行在其它过滤器和Action方法之前。客户端请求在调用Action之前,MVC框架会检测Action上是否有授权过滤器,如果有会调用OnAuthorization方法,如果此方法批准了请求,才会调用相应的Action。流程如图:

graph TD
A(浏览器发起请求) --> B[路由器解析URL,调用对应控制器方法]
    B --> C{是否有授权过滤器}
    C --> |有| D{调用OnAuthorization方法验证}
    D --> | 授权通过 | E
    D --> | 授权未通过 | F[拒绝URL请求]
    C --> |没有| E[响应URL请求]

MVC默认使用AuthorizeAttribute实现了IAuthorizationFilter接口,所以我们可以在Action方法上直接添加Authorize特性标签来验证授权:

打开Index页面,会显示无权限:

由于使用的是MVC自带的授权验证方法,未能符合它的验证机制,所以无权限查看。通常我们需要添加一个新的派生自AuthorizeAttribute类的授权过滤器来完成我们自己业务逻辑。

下面我们自定义一个授权过滤器。我们在MVC项目中添加一个Filters文件夹,我们所有自定义的过滤器都放可以到这个文件夹下,便于管理。

在Filters下创建一个类,类名为MyAuthorizeAttribute。需要注意,过滤器要以Attribute结尾,这是MVC的约定。代码如下:

public class MyAuthorizeAttribute : AuthorizeAttribute
{
    //重写授权检查方法,返回值为true,允许访问,false,禁止访问。
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        //请求参数user为空,禁止访问
        if (string.IsNullOrEmpty(HttpContext.Current.Request.QueryString["user"]))
        {
            return false;
        }
        return true;
    }
}

可以看到,我们只要重写AuthorizeCore方法就可以根据我们的业务需求判断是否有权限访问,返回值为true允许访问,返回值为false禁止访问。

回到HomeController,我们给About方法加上我们自定义的特性:

我们看看效果:
[video width="1138" height="700" mp4="http://www.yunc.top/wp-content/uploads/2019/12/filterAuthorize.mp4"]

可以看到,当About页面没有user参数时,会提示无权限,有user参数则可以访问通过。

在实际业务中我们可以使用授权过滤器来管理用户登录状态的授权验证。当然,我的这个例子只是基础的用法,实际业务比这复杂的多,那么就需要我们自己去思考设计授权过滤器方法了。

动作过滤器

动作过滤器需要实现IActionFilter接口,接口定义如下:

//
// 摘要:
//     定义操作筛选器中使用的方法。
public interface IActionFilter
{
    //
        // 摘要:
        //     在执行操作方法后调用。
        //
        // 参数:
        //   filterContext:
        //     筛选器上下文。
        void OnActionExecuted(ActionExecutedContext filterContext);
    //
        // 摘要:
        //     在执行操作方法之前调用。
        //
        // 参数:
        //   filterContext:
        //     筛选器上下文。
        void OnActionExecuting(ActionExecutingContext filterContext);
}

我们看到该接口里有两个方法OnActionExecuting和OnActionExecuted,前者在动作方法执行前调用,后者在动作方法执行后调用。

OnActionExecuting方法是在Action方法执行前调用的,那么我们可以利用这个方法来检测请求,并且可以在这里修改请求,取消请求等等操作。

客户端的请求信息是一个ActionExecutingContext对象,它继承自ControllerContext类,属性如下:

名称 类型 说明
ActionDescriptor ActionDescriptor 获取或设置操作描述符。
ActionParameters IDictionary<string, object> 获取或设置操作方法参数。
Result ActionResult 获取或设置由操作方法返回的结果。

我们添加一个自定义的Action过滤器。由于ActionFilterAttribute类实现了IActionFilter接口,所以我们直接继承ActionFilterAttribute类即可,并且重写OnActionExecuting和OnActionExecuted方法。如下:

public class MyActionAttribute : ActionFilterAttribute
{
    /// <summary>
    /// Action调用之前运行
    /// </summary>
    /// <param name="filterContext"></param>
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (string.Equals(filterContext.HttpContext.Request.HttpMethod, "get", StringComparison.CurrentCultureIgnoreCase))
        {
            filterContext.Result = new HttpNotFoundResult("只允许POST请求!");
        }
    }
    /// <summary>
    /// Action调用之后运行
    /// </summary>
    /// <param name="filterContext"></param>
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {

    }
}

我们给Index方法添加上MyAction特性标签:

打开Index页,显示如下:

可以看到,页面返回了404错误,提示信息为我们设置的Message。

OnActionExecuted方法在Action操作方法调用之后执行,传递给OnActionExecuted方法的参数是ActionExecutedContext对象。这个类比ActionExecutingContext对象多了些属性,如下:

名称 类型 说明
ActionDescriptor ActionDescriptor 获取或设置操作描述符。
Canceled bool 获取或设置一个值,该值指示此ActionExecutedContext 对象已被取消。
Exception Exception 获取或设置在操作方法的执行过程中发生的异常(如果有)。
ExceptionHandled bool 获取或设置一个值,该值指示是否处理异常。
Result ActionResult 获取或设置由操作方法返回的结果。

我们可以通过OnActionExecuted方法来执行一些跨越动作方法的任务,比如我们可以用它来获取动作方法执行的时间。我们修改 MyActionAttribute 过滤器代码如下:

public class MyActionAttribute : ActionFilterAttribute
{
    private Stopwatch timer;

    /// <summary>
    /// Action调用之前运行
    /// </summary>
    /// <param name="filterContext"></param>
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        timer = Stopwatch.StartNew();
    }
    /// <summary>
    /// Action调用之后运行
    /// </summary>
    /// <param name="filterContext"></param>
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        timer.Stop();
        filterContext.HttpContext.Response.Write($"<div>方法执行时间:{timer.Elapsed.TotalSeconds:F6}s</div>");
    }
}

我们在方法启动之前启动了一个计时器,在方法执行后停止了它,并且将这个时间间隔输出到我们的页面上。重新编译打开Index页面,显示如下:

结果过滤器

结果过滤器,顾名思义针对的是动作方法返回的结果,它在我们的动作方法结果返回前后执行。

创建结果过滤器需要实现IResultFilter接口。ActionFilterAttribute类帮我们实现了IResultFilter接口,我们可以直接继承ActionFilterAttribute创建我们的过滤器,然后通过重写OnResultExecutin和OnResultExecuting(在执行操作结果后调用)方法来实现过滤器规则。

OnResultExecuting方法会在执行操作结果前调用,这个方法的参数是一个ResultExecutingContext对象,属性如下:

名称 类型 说明
Cancel bool 获取或设置一个值,该值指示此 ResultExecutingContext 值是否为“cancel”。
Result ActionResult 获取或设置操作结果。

OnResultExecuted方法在执行操作结果后调用,这个方法的参数是一个ResultExecutingContext对象,属性如下:

名称 类型 说明
Canceled bool 获取或设置一个值,该值指示此 ResultExecutingContext 值是否为“cancel”。
Exception Exception 获取或设置在操作方法的执行过程中发生的异常(如果有)。
ExceptionHandled bool 获取或设置一个值,该值指示是否处理异常。
Result ActionResult 获取或设置操作结果。

我们可以使用这两个方法在Action方法返回结果前后进行操作,具体操作的代码我就不赘述了。

异常处理过滤器

异常处理过滤器需要实现的接口为IExceptionFilter。我们看下接口的定义:

//
// 摘要:
//     定义异常筛选器所需的方法。
public interface IExceptionFilter
{
    //
    // 摘要:
    //     在发生异常时调用。
    //
    // 参数:
    //   filterContext:
    //     筛选器上下文。
    void OnException(ExceptionContext filterContext);
}

接口方法OnException可以看到它在我们的方法中出现异常时触发,MVC默认用HandleErrorAttribute类来实现了此接口,我们自己定义的异常过滤器可以继承此类进行扩展。

OnException方法中传递的参数是一个ExceptionContext对象,它的属性如下:

名称 类型 说明
Exception Exception 获取或设置异常对象。
ExceptionHandled bool 获取或设置一个值,该值指示是否已处理异常。
Result ActionResult 获取或设置操作结果。

我们来定义一个异常过滤器,代码如下:

public class MyExceptionAttribute : HandleErrorAttribute
{
    public override void OnException(ExceptionContext filterContext)
    {
        //如果有异常,跳转到异常页面。
        if (filterContext.Exception != null)
        {
            //跳转到自定义的错误页
            ActionResult view = new ViewResult() { ViewName = "Error" };
            filterContext.Result = view;
            //异常处理结束后,一定要将ExceptionHandled设置为true,否则仍然会继续抛出错误。
            filterContext.ExceptionHandled = true;
        }
    }
}

当我们的方法中出现异常时,会将Views文件夹下Shared中的Error.cshtml页面返回到客户端,客户端页面不再显示成黄页,给用户一个良好的体验。

我们给Index方法添加上我们的异常处理器,再加一段引发异常的代码看下效果。

打开Index视图:

视图中显示的是Error页面中的内容。

通常我们用异常处理器来记录我们的程序异常日志,或者在产生异常时给客户端返回一个友好的提示内容。

过滤器的使用方法

在上文的一些例子中,我们把过滤器的特性都定义在了Action方法上,其实过滤器不仅可以应用在Action方法中,还可在应用在Controller和全局配置中。

应用在Controller中的使用方法和Action一直,在Controller类名上方添加特性标签即可。如图:

全局环境下的过滤器,则需要注册到FilterConfig文件中,例如MVC默认给我们注册的HandleErrorAttribute异常处理器:

并且我们可以注册很多个过滤器在全局环境下,那么在此注册的过滤器会应用到整个应用程序当中。

总结

本章对过滤器的类型,作用,定义以及使用方法做了一些说明,当然这些都是比较基础的内容,真正深入的理解还得多多使用,如果文章中有错误或者不足的地方,请大家在评论中指正出来。

更多内容可访问我的博客:http://www.yunc.top/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容