《Pro ASP.NET MVC 3 Framework》学习笔记之二十五【Filters】

简介: 过滤器(Filters) 过滤器(Filters)向请求处理管道注入了额外的逻辑。他们提供了一种简单而优雅的方式实现了横切关注点,这个术语是针对整个应用程序使用的功能,并不能灵活的适用任何一个点,所以这个会打破分解关注点的模式。

过滤器(Filters)

过滤器(Filters)向请求处理管道注入了额外的逻辑。他们提供了一种简单而优雅的方式实现了横切关注点,这个术语是针对整个应用程序使用的功能,并不能灵活的适用任何一个点,所以这个会打破分解关注点的模式。像日志,验证和缓存都是经典的横切关注点的例子。

之所以称为过滤器(Filters),是因为这个术语同样应用于其他web应用程序框架里面,包括Ruby on Rails。然而,MVC框架里面的过滤器完全不同于ASP.NET平台里面的Request.Filters和Response.Filter对象,这两个对象是实现请求和响应流的传输(一种高级的并很少发生的活动)。当然,我们能够在MVC框架里面使用这两个对象,但通常我们谈及过滤器是指MVC框架里面的过滤器。本章里面会介绍MVC框架所支持的不同的过滤器策略以及如何控制它们执行。

使用Filters

在前面的SportsStore项目里面已经使用了过滤,就是将验证应用到了Controller的action方法里面,只有验证的用户才能请求相应的action方法,这为我们提供了一种选择的途径。我们可以在每一个action方法里面检查请求的验证状态。如下所示:

View Code
namespace SportsStore.WebUI.Controllers { 

public class AdminController : Controller {

// ...

public ViewResult Index() {
if (!Request.IsAuthenticated) {
FormsAuthentication.RedirectToLoginPage();
}
// ...
}
public ViewResult Create() {
if (!Request.IsAuthenticated) {
FormsAuthentication.RedirectToLoginPage();
}
// ...
}

public ViewResult Edit(int productId) {
if (!Request.IsAuthenticated) {
FormsAuthentication.RedirectToLoginPage();
}
// ...
}

// ...
}
}

通过上面的代码,我们可以发现其实里面有很多重复的地方,那使用Filters就能解决这个问题。如下代码所示:

View Code
namespace SportsStore.WebUI.Controllers { 

[Authorize]
public class AdminController : Controller {

// ...

public ViewResult Index() {

// ...
}

public ViewResult Create() {
// ...
}

public ViewResult Edit(int productId) {
// ...
}

// ...
}
}

Filters是.NET里面的特性(Attributes),这些是添加到请求处理管道额外的步骤。上面使用了Authorize过滤器实现同样的效果,但是代码显然更加简捷优雅。有四种基本的过滤器类型:Authorization, Action, Result, Exception对应接口为IAuthorizationFilter,IActionFilter,IResultFilter,IExceptionFilter。

在MVC框架调用一个Action之前,它会检查方法的定义中是否有实现了上面接口的特性(Attributes).如果有的话,那么在请求处理管道适当的位置,通过该接口定义的方法会被调用。MVC框架包含了默认已经实现了过滤器接口的Attributes类。

注:ActionFilterAttribute类实现了IActionFilter和IResultFilter接口,这个类是抽象类。其他的像AuthorizeAttribute和HandleErrorAttribute,包含了有用的功能,不需要派生出一个类就可以直接使用。

对Controllers和Action方法应用过滤器

Filters可以应用到单个的Action方法或者是整个Controller类,如下所示:

View Code
namespace SportsStore.WebUI.Controllers { 

public class AdminController : Controller {

// ... instance variables and constructor
[Authorize]
public ViewResult Index() {
if (!Request.IsAuthenticated) {
FormsAuthentication.RedirectToLoginPage();
}
// ...rest of action method
}

[Authorize]
public ViewResult Create() {
if (!Request.IsAuthenticated) {
FormsAuthentication.RedirectToLoginPage();
}
// ...rest of action method
}

// ... other action methods
}
}

我们可以使用多个Filters,也可以混合使用Filters。如下所示:

View Code
[Authorize(Roles="trader")] // applies to all actions 
public class ExampleController : Controller {

[ShowMessage] // applies to just this action
[OutputCache(Duration=60)] // applies to just this action
public ActionResult Index() {
// ... action method body
}
}

有一些过滤器获取几个参数,后面会有探究其原理。
注:如果我们自定义了针对Controllers的基类,任何应用在基类的过滤器会对其所有的派生类起作用。

使用Authorization过滤器

Authorization过滤器是在其他的过滤器之前运行,并且也是在Action方法被调用之前。这个过滤器是用来授权认证的,确保只有授权的用户才能调用。看一下这样一个场景,MVC框架接收一个来自浏览器的请求,路由系统处理请求的URL并提取被命中的controller和action的名字。一个新的controller实例被创建,但是在action方法被调用之前,MVC框架会检查是否有应用到该action方法上面的授权认证过滤器(authorization filters)。如果有,那么定义在IAuthorizationFilter接口里面的唯一的方法OnAuthorization会被调用。如果authentication filter批准了该请求,那么请求管道里面的下一步就会被执行,否则请求就会被拒绝。

创建一个授权认证过滤器(Creating an Authentication Filter)

下面是一个检查用户是否登录的过滤器,如下所示:

View Code
namespace MvcFilters.Infrastructure.Filters 
{
public class CustomAuthAttribute : AuthorizeAttribute
{
private string[] allowedUsers;
public CustomAuthAttribute(params string[] users)
{
allowedUsers = users;
}

protected override bool AuthorizeCore(HttpContextBase httpContext)
{
return httpContext.Request.IsAuthenticated &&
allowedUsers.Contains(httpContext.User.Identity.Name,
StringComparer.InvariantCultureIgnoreCase);
}
}
}

我们定义的过滤器需要一个数组参数,数组里面包含了被授权认证的用户,我们的过滤器里面包含了PerformAuthenticationCheck方法,确保请求是被认证的并用户也是在授权的集合里面的。这个类里面最有趣的部分是OnAuthorization方法的实现,传递给该方法的参数是AuthorizationContext类的实例,这个类是从ControllerContext派生。这让我们可以访问一些有用的对象,这些对象包含:Controller,HttpContext,IsChildAction,RequestContext,RouteData。

回想下,当我们判断请求是否通过验证用到的方法:filterContext.HttpContext.Request.IsAuthenticated。使用Context对象,我们能获取需要对请求做决断的所有信息。

AuthorizationContext定义了两个额外的属性:

1.ActionDescriptor(类型是ActionDescriptor)提供Action方法的详情
2.Result(类型是ActionResult)Action方法的返回结果,是一个通过设置这个属性值为non-null来取消请求的过滤器

第一个属性ActionDescriptor返回的是System.Web.Mvc.ActionDescriptor类的实例,我们能够用来获取应用了过滤器的action方法的有关信息。
第二个属性Result,是让过滤器工作的关键。当我们给一个针对action请求授权,然后我们在OnAuthorization里面什么都不做,这会让MVC框架默认请求应该被处理。然而如果我们设置Result属性为一个ActionResult对象,MVC框架会使用它作为所有请求的结果,在请求管道里剩余的步骤也不会执行,并且我们提供的结果会为用户生成输出。

我们的例子里面,如果PerformAuthenticationCheck返回false(表明请求没有被验证或用户没有被授权),那么我们创建一个HttpUnauthorizedResult结果并赋给Result属性,如:filterContext.Result = new HttpUnauthorizedResult();

使用我们自定义的过滤器如下:

View Code
... 
[CustomAuth("adam", "steve", "bob")]
public ActionResult Index() {
return View();
}
...

使用内置的授权过滤器(Using the Built-in Authorization Filter)

MVC框架里面包含了一个非常有用的内置的授权过滤器称为AuthorizeAttribute。我们可以具体通过两个属性来设置验证策略。
两个公开的属性:1.Users(类型String)  2.Roles(类型String)。示例代码如下:

View Code
... 
[Authorize(Users="adam, steve, bob", Roles="admin")]
public ActionResult Index()
{
return View();
}
...

上面的过滤器表示只有用户是  adam,  steve, 和bob的,并且用户有管理员权限的才能请求该action方法。

对大多数应用程序,AuthorizeAttribute提供的验证策略已经足够使用。如果我们想实现比较特殊的过滤器,可以从这个类派生。相对于直接实现IAuthorizationFilter接口的方式,这种风险更低。AuthorizeAttribute类提供两个可以自己现实的点。分别如下:

1.AuthorizeCore方法:被OnAuthorization的AuthorizeAttribute实现调用以及实现授权检查
2.HandleUnauthorizedRequest方法:当授权检查失败时调用

注:我们能够重写的第三个方法是OnAuthorization,推荐不要这样做。因为这个方法默认的实现包含了对内容缓存使用输出缓存过滤器的安全地处理。后面会介绍

实现一个自定义授权策略

下面创建一个自己的AuthorizeAttribute子类。这个策略将要授权任何从服务器桌面直接运行浏览器(Request.IsLocal为true),或者远程用户的用户名能够匹配normalAuthorizeAttribute的规则。这对于服务器管理员绕开网站的登录过程非常有用。代码实现如下:

View Code
using System.Web; 
using System.Web.Mvc;

namespace MvcFilters.Infrastructure.Filters
{
public class OrAuthorizationAttribute : AuthorizeAttribute
{

protected override bool AuthorizeCore(HttpContextBase httpContext)
{
return httpContext.Request.IsLocal || base.AuthorizeCore(httpContext);
}
}
}

可以这样使用这个过滤器:[OrAuthorization(Users = "adam, steve, bob", Roles = "admin")]

实现一个自定义的授权失败策略

默认的处理为通过授权的做法是重定向用户到登录页面。但是我不想一直这么做,例如,如果我们使用AJAX,发送一个重定向能让登录页出现在当前页面的中间(类似模态窗口这样的效果)。这时我们可以重写HandleUnauthorizedRequest方法来实现,如下所示:

View Code
using System.Web.Mvc; 

namespace MvcFilters.Infrastructure.Filters {
public class AjaxAuthorizeAttribute : AuthorizeAttribute {

protected override void HandleUnauthorizedRequest(AuthorizationContext context) {

if (context.HttpContext.Request.IsAjaxRequest()) {
UrlHelper urlHelper = new UrlHelper(context.RequestContext);
context.Result = new JsonResult {
Data = new {
Error = "NotAuthorized",
LogOnUrl = urlHelper.Action("LogOn", "Account")
}, JsonRequestBehavior = JsonRequestBehavior.AllowGet};
} else {
base.HandleUnauthorizedRequest(context);
}
}
}
}

使用异常过滤器(Using Exception Filters)

异常过滤器是在调用一个action方法抛出了无法处理的异常时运行。
创建一个异常过滤器(Creating an Exception Filter)
异常过滤器必须实现IExceptionFilter接口:public interface IExceptionFilter {void OnException(ExceptionContext filterContext);}   OnException方法在异常抛出的时候调用。参数是ExceptionContext对象。下面是一个示例:

View Code
using System.Web.Mvc; 
using System;

namespace MvcFilters.Infrastructure.Filters {

public class MyExceptionAttribute: FilterAttribute, IExceptionFilter {

public void OnException(ExceptionContext filterContext) {

if (!filterContext.ExceptionHandled &&
filterContext.Exception is NullReferenceException) {

filterContext.Result = new RedirectResult("/SpecialErrorPage.html");
filterContext.ExceptionHandled = true;
}
}
}
}

这个过滤器响应NullReferenceException异常的实例,并在没有其他异常过滤器显示异常被处理的情况下执行。我们重定向用户到错误页面。

使用内置的异常过滤器

HandleErrorAttribute就是内置的实现了IExceptionFilter接口的过滤器,并是创建异常过滤器变得容易。

当一个未处理的并指定了ExceptionType的异常发生时,过滤器会设置HTTP结果代码为500(服务器错误),并呈现在View属性里面指定的视图,如下所示:

[HandleError(ExceptionType=typeof(NullReferenceException), View="SpecialError")]

注意:HandleErrorAttribute只有在web.config文件里面添加了<customErrors mode="On" />才能工作。默认的为RemoteOnly,这意味着在开发过程中HandleErrorAttribute不会拦截异常,当部署到服务器并从另外的计算机发送请求是才起作用。

当呈现一个视图时,HandleErrorAttribute过滤器会传递一个HandleErrorInfo视图模型对象,如下所示:

View Code
@Model HandleErrorInfo 
@{
ViewBag.Title = "Sorry, there was a problem!";
}

<p>
There was a <b>@Model.Exception.GetType().Name</b>
while rendering <b>@Model.ControllerName</b>'s
<b>@Model.ActionName</b> action.
</p>
<p>
The exception message is: <b><@Model.Exception.Message></b>
</p>
<p>Stack trace:</p>
<pre>@Model.Exception.StackTrace</pre>

使用Action和Result过滤器

Action和Result过滤器是过多用途的过滤器,可用于任何意图。为了创建这些过滤器类型的内置类,IActionFilter,实现了以上接口。如下:

View Code
namespace System.Web.Mvc { 

public interface IActionFilter {
void OnActionExecuting(ActionExecutingContext filterContext);
void OnActionExecuted(ActionExecutedContext filterContext);
}
}

接口定义了两个方法,MVC框架在调用Action方法之前调用OnActionExecuting方法,OnActionExecuted方法则是在Action方法调用完成后被调用。

实现OnActionExecuting方法

OnActionExecuting方法在Action方法之前调用,所以,我们可以利用这个机会来检查请求,取消请求,修改请求,或者启用一些活动来跨越action方法。参数类型ActionExecutingContext是ControllerContext的子类并定义了两个属性:ActionDescriptor和Result。我们可以通过Result属性选择性的取消请求,如下所示:

View Code
using System.Web.Mvc; 

namespace MvcFilters.Infrastructure.Filters {

public class MyActionFilterAttribute : FilterAttribute, IActionFilter {

public void OnActionExecuting(ActionExecutingContext filterContext) {
if (!filterContext.HttpContext.Request.IsSecureConnection) {
filterContext.Result = new HttpNotFoundResult();
}

}

public void OnActionExecuted(ActionExecutedContext filterContext) {
// do nothing
}
}
}

上面的例子用来检查请求是否使用了SSL,如果没有则返回404.

实现OnActionExecuted方法

我们也可以使用这个过滤器来执行一些跨越action方法执行的任务,下面的示例计算action方法执行的时间,如下:

View Code
using System.Diagnostics; 
using System.Web.Mvc;

namespace MvcFilters.Infrastructure.Filters {

public class ProfileAttribute : FilterAttribute, IActionFilter {
private Stopwatch timer;
public void OnActionExecuting(ActionExecutingContext filterContext) {
timer = Stopwatch.StartNew();
}

public void OnActionExecuted(ActionExecutedContext filterContext) {
timer.Stop();
if (filterContext.Exception == null) {
filterContext.HttpContext.Response.Write(
string.Format("Action method elapsed time: {0}",
timer.Elapsed.TotalSeconds));
}
}
}
}

实现结果过滤器(Implementing a Result Filter)

Action filters和Result filters有很多共同点。它要实现IResultFilter接口,如下:

View Code
namespace System.Web.Mvc { 

public interface IResultFilter {
void OnResultExecuting(ResultExecutingContext filterContext);
void OnResultExecuted(ResultExecutedContext filterContext);
}
}

一旦action方法返回了一个action result,OnResultExecuting方法就会被调用,但这是在action result执行之前,OnResultExecuted方法是在action result执行之后。
示例如下:

View Code
using System.Diagnostics; 
using System.Web.Mvc;

namespace MvcFilters.Infrastructure.Filters {
public class ProfileResultAttribute : FilterAttribute, IResultFilter {
private Stopwatch timer;

public void OnResultExecuting(ResultExecutingContext filterContext) {
timer = Stopwatch.StartNew();
}

public void OnResultExecuted(ResultExecutedContext filterContext) {
timer.Stop();
filterContext.HttpContext.Response.Write(
string.Format("Result execution - elapsed time: {0}",
timer.Elapsed.TotalSeconds));
}
}
}

使用内置的Action和Result过滤器类

MVC框架包含了能够创建action和result过滤器的内置类,但是没有提供任何有用的功能,这个ActionFilterAttribute类如下所示:

View Code
public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IResultFilter{ 

public virtual void OnActionExecuting(ActionExecutingContext filterContext) {
}

public virtual void OnActionExecuted(ActionExecutedContext filterContext) {
}

public virtual void OnResultExecuting(ResultExecutingContext filterContext) {
}

public virtual void OnResultExecuted(ResultExecutedContext filterContext) {
}
}
}

下面是一个从它派生的过滤器,如下:

View Code
using System.Diagnostics; 
using System.Web.Mvc;

namespace MvcFilters.Infrastructure.Filters {

public class ProfileAllAttribute : ActionFilterAttribute {
private Stopwatch timer;

public override void OnActionExecuting(ActionExecutingContext filterContext) {
timer = Stopwatch.StartNew();
}

public override void OnActionExecuted(ActionExecutedContext filterContext) {
timer.Stop();
filterContext.HttpContext.Response.Write(
string.Format("Action method elapsed time: {0}",
timer.Elapsed.TotalSeconds));
}
public override void OnResultExecuting(ResultExecutingContext filterContext) {
timer = Stopwatch.StartNew();
}

public override void OnResultExecuted(ResultExecutedContext filterContext) {
timer.Stop();
filterContext.HttpContext.Response.Write(
string.Format("Action result elapsed time: {0}",
timer.Elapsed.TotalSeconds));
}
}
}

使用其他过滤功能

除了上面介绍的过滤器以外,还有一些非常有趣但是用的并不那么广泛的过滤功能。如下:

不使用特性的过滤

使用过滤器常规的方式是创建和使用特性(attributes),然而有一种可以替代的方式。Controller类实现IAuthorizationFilter,  IActionFilter,  IResultFilter, IExceptionFilter这些接口,同时也提供了每一种OnXXX方法的虚拟实现。下面是计算action方法执行时间的实现,如下:

View Code
using System.Diagnostics; 
using System.Web.Mvc;

namespace MvcFilters.Controllers {

public class SampleController : Controller {
private Stopwatch timer;

public ActionResult Index() {
return View();
}

protected override void OnActionExecuting(ActionExecutingContext filterContext) {
timer = Stopwatch.StartNew();
}

protected override void OnActionExecuted(ActionExecutedContext filterContext) {
timer.Stop();
filterContext.HttpContext.Response.Write(
string.Format("Action method elapsed time: {0}",
timer.Elapsed.TotalSeconds));
}

protected override void OnResultExecuting(ResultExecutingContext filterContext) {
timer = Stopwatch.StartNew();
}

protected override void OnResultExecuted(ResultExecutedContext filterContext) {
timer.Stop();
filterContext.HttpContext.Response.Write(
string.Format("Action result elapsed time: {0}",
timer.Elapsed.TotalSeconds));
}
}
}

这种技术在我们创建一个基类,该基类从项目里面多个控制器派生出来。过滤的重点就应用程序所需代码放在一个可重复使用的位置。对我们的项目而言,我们宁愿使用特性。这样能够将控制逻辑和过滤逻辑分开。

使用全局过滤

全局过滤是应用到所有的action方法,通过如下的方式实现:

public class MvcApplication : System.Web.HttpApplication { 

public static void RegisterGlobalFilters(GlobalFilterCollection filters)//在Application_Start里面调用
{
filters.Add(new HandleErrorAttribute());
filters.Add(new ProfileAllAttribute()); //注意这里一定是完整的类名,不能是ProfileAll。一旦我们在这里注册了过滤器,将应用与所有的action
}
...

过滤执行排序

前面解释了过滤器按类型被执行,顺序是:authorization过滤器,action过滤器,result过滤器。如果在执行的任何一步出现未处理的异常,异常过滤器都会被执行。然而在每一个类型里面,我们能够控制个别的过滤器的顺序。如下:

View Code
using System; 
using System.Web.Mvc;

namespace MvcFilters.Infrastructure.Filters {

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple=true)]
public class SimpleMessageAttribute : FilterAttribute, IActionFilter {

public string Message { get; set; }

public void OnActionExecuting(ActionExecutingContext filterContext) {
filterContext.HttpContext.Response.Write(
string.Format("[Before Action: {0}]", Message));
}

public void OnActionExecuted(ActionExecutedContext filterContext) {
filterContext.HttpContext.Response.Write(
string.Format("[After Action: {0}]", Message));
}
}
}

在action里面使用如下:

View Code
... 
[SimpleMessage(Message="A")]
[SimpleMessage(Message="B")]
public ActionResult Index() {
Response.Write("Action method is running");
return View();
}
...

运行结果如下:

如果我们要指定顺序可以这样:

View Code
... 
[SimpleMessage(Message="A", Order=2)]
[SimpleMessage(Message="B", Order=1)]
public ActionResult Index() {
Response.Write("Action method is running");
return View();
}
...

运行程序如下:

如果我们不给order指定一个具体的值,默认会是-1。如果我们混淆了过滤器以至于一些有其他的值,一些没有,这些没有值的将会首先被执行。如果多个filters有同样的值,那么MVC框架根据它们被使用的地方来决定执行顺序。全局过滤器最先执行,然后应用在controller类上面的,最后是action上面的过滤器执行。

使用内置的过滤器

MVC框架提供了一些内置的过滤器,以备我们在程序里面直接使用。如下所示:


好了,本章的笔记到这就结束了,祝大家愉快!

相关文章