Filter在请求管道注入额外的逻辑。他们提供简单优雅的方法实现横切点关注。这个术语指的是在穿越整个应用程序中使用,而且不适合使用在任何单独的地方,所以这会打破关注模式的分离。经典的横切点关注的例子比如日志,认证,缓存。

Filter也被认为是横切点关注,因为这个术语在其他web application框架,包括Ruby也是实现同样功能。然而MVC Framework Filter完全不同于ASP.NET 平台的Request.Filter和Response.Filter对象。Request.Filter和Response.Filter对象在request和response流上执行转换。你可以在MVC application中使用Request.Filter 和Response.Filter,但是,通常当ASP.NET MVC程序员谈到filter时,指的是接下要谈的类型,在本文,我们会展示MVC Framework提供的filter不同的之类,如何创建使用filter,如何控制他们的执行。

使用Filter

我们想要让action方法只能由认证过的用户使用。我们可以在每个action方法检查请求的认证状态。如下面的代码,显式的在action方法检测认证信息。

namespace SportsStore.WebUI.Controllers {

public class AdminController : Controller {

// ... instance variables and constructor

public ViewResult Index() {

if (!Request.IsAuthenticated) {

    FormsAuthentication.RedirectToLoginPage();

}

// ...rest of action method

}

public ViewResult Create() {

if (!Request.IsAuthenticated) {

FormsAuthentication.RedirectToLoginPage();

}

// ...rest of action method

}

public ViewResult Edit(int productId) {

if (!Request.IsAuthenticated) {

    FormsAuthentication.RedirectToLoginPage();

}

// ...rest of action method

}

// ... other action methods

}

}

可以看到这方法中有很多重复代码,所以我们决定使用filter。如下代码:

namespace SportsStore.WebUI.Controllers {

[Authorize]

public class AdminController : Controller {

// ... instance variables and constructor

public ViewResult Index() {

// ...rest of action method

}

public ViewResult Create() {

// ...rest of action method

}

public ViewResult Edit(int productId) {

// ...rest of action method

}

// ... other action methods

}

}

Filter是.NET特性,它是加在request处理管道上的额外步骤。上例中,我们使用Authorize filter,和之前那种重复检查的方法,有相同的效果。

特性是从System.Attribute继承的特殊的.NET类。你可以把它们附加到其它代码上,包括类,方法,属性,字段。目的就是在编译的代码中植入额外的信息,在运行事情你就可以读回它们。

C#中,特性使用的时候要加上方括号,你能通过命名参数语法组合它们的公共属性(比如,[MyAttribute(SomeProperty=value)])。在C#编译器命名约定中,如果特性类以单词Attribute结尾,你可以忽略这部分(比如,应用AuthorizeAttribute时,可以直接写[Authorize])

Filter的四种基本类型

MVC Framework 支持四种不同的filter类型。每个都允许你在请求管道的不同点处引入自己的逻辑,下表列出了这四种类型。

8f4b147249f746fa8439b0ae296b9eb7[4]

在MVC Framework调用action之前,它会检查方法定义,查看实现了上述表格中的接口的特性是否存在。如果存在,那么在request管道的恰当处,有这些接口定义的方法就会被调用。framework包括了实现filter接口的默认的特性类。

注意ActionFilterAttribute类实现了IActionFilter和IResultFilter 接口。此类是抽象类,迫使你提供一个实现。其它类,AuthorizeAttribute和HandleErrorAttribute包含了有用的功能,可以不需要继承直接使用。

在controller和action方法上应用Filter

Filter可以应用在单个action方法上,或者整个controller上,在上面的例子,我们把Authorize filter应用于 AdminController类,这和应用在每一个controller中的action方法有同样的效果,如下:

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

}

}

你可以应用多个filter,无论他们匹配到controller或者单个action方法。下面展示3种不同的使用filter的方法。

[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

}

}

注意,如果你为controller自定义了基类,那么任何应用在基类上的filter也会应用在子类上。

使用Authorization Filter

Authorization filter 是最先运行的filter,它在其他filter和action方法之前被调用,就如它的命名,这些filter强迫你的认证策略,确保action方法只能被认证过的用户调用。Authorization filter 实现IAuthorizationFilter 接口,如下代码:

namespace System.Web.Mvc {

public interface IAuthorizationFilter {

void OnAuthorization(AuthorizationContext filterContext);

}

}

MVC Framework从浏览器收到一个请求,路由系统已经处理了请求的URL,抽取了controller和action的名字。一个controller的实例创建,但是在action方法调用之前,MVC Framework检查是否有任何authorization filter应用在action方法。如果有,那么这个由IAuthorizationFilter接口定义的方法——OnAuthorization方法会被调用,如果authentication filter同意了请求,那么处理管道中下一步才会开始,否则请求就会拒绝。

创建Authentication Filter

理解authentication filter工作原理的最好方法就是创建一个。下面的代码是一段简单的示例。它仅仅检查用户是否登录了。(Request.IsAuthenticated是ture),同时username要出现在允许用户列表中。

using System;

using System.Linq;

using System.Web.Mvc;

using System.Web;

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);

}

}

}

这个创建了一个authorization filter最简单的方法是继承AuthorizeAttribute类,重写AuthorizeCore方法。这确保了从AuthorizeAttribute内建功能中获益。

警告:编写安全保障代码是危险行为

我们写了一个自定义authorization filter的例子,因为我们认为它可以展示filter的运行方式,但是对于编写自己安全代码,我们要小心。这种技能很少有人会去使用,这会让我们的应用程序安全出现未考虑到的漏洞。

我们尽可能的使用那些广泛使用,并且被实践过的安全代码,在这种情况下,MVC Framework提供了一个功能全面的authorization filter,可以通过继承它来实现自定义认证策略。我们尽可能使用它,并且我们建议你也这样做,这样,当你的安全数据在网上泄露了,你至少可以骂微软。

我们的filter构造方法带有一组名字的数组,这数组内容是被认证的用户的信息。我们的filter包含一个PerformAuthenticationCheck方法,确保请求是认证的,用户也是在已认证的集合中。这个类中有趣的部分是OnAuthorization方法的实现。传给这个方法的参数是AuthorizationContext 类的一个实例,AuthorizationContext是继承于ControllerContext的。ControllerContext 能使我们访问一些有用的对象,不仅仅是HttpContextBase(通过HttpContextBase,我们可以访问request的细节)。ControllerContext的属性在下表中列出,所以这些用于不同的action filer的context 对象都继承自这个类,所以你可以始终如一的使用这些属性。

bcb46d9c737f4f5b9acf75d1074260f0[4]

当我们想检查是否请求被认证了,我们就如下的写法:

... filterContext.HttpContext.Request.IsAuthenticated ...

AuthorizationContext定义了2个额外的属性,如下表:

5960cf95fcb4489682d9e494e8f70f9f[4]

第一个属性ActionDescriptor,返回一个System.Web.Mvc.ActionDescriptor实例,你可以通过这个属性得到你应用了filter的action的信息。第二个属性,Result,是让你filter工作的关键。如果你要对一个action方法认证请求,那么你在OnAuthorization方法上不用做什么,MVC Framework认为这个请求应该处理。但是,如果你设置context对象的Result属性为一个ActionResult对象,MVC

Framework将使用使用它作为整个请求的结果。管道中剩下的步骤则不会被运行了,你提供的result会运行输出给用户。

在我们的例子中,如果我们的PerformAuthenticationCheck返回false(表面整个请求没有认证通过,或者用户不是认证用户),然后我们创建一个HttpUnauthorizedResultaction result,然后制定context的 Result 属性,如下:

filterContext.Result = new HttpUnauthorizedResult();

要使用我们的自定义authorization filter。我们只需要将特性放在action方法上,如下代码:

...

[CustomAuth("adam", "steve", "bob")]

public ActionResult Index() {

return View();

}

...

使用内建的认证Filter

MVCFramework包含了一个实用的认证filter,AuthorizeAttribute。我们可以使用两个公共属性指定认证策略,如下:

2b238e6b2ba445d8bf706c8065f9dd96[4]

...

[Authorize(Users="adam, steve, bob", Roles="admin")]

public ActionResult Index() {

return View();

}

...

上例中,我们指定了用户和权限。这意味着只有当用户和权限同时满足的情况下才认证系统才会通过。这里还有一个隐式的条件,就是这个请求必须是被认证的。如果我们不指定任何用户和权限,那么任何用户都可使用此action方法。

对大多数应用程序来说,AuthorizeAttribute提供的认证策略已经足够了。如果你需要实现一些特殊的功能,你可以继承这个类。直接实现IAuthorizationFilter接口风险比较小,但是你应该仔细思考,你的策虑所带来的影响,而且要彻底的测试。

AuthorizeAttribute类提供2个不同方法。

AuthorizeCore方法,由AuthorizeAttribute的OnAuthorization调用,实现认证检查。

HandleUnauthorizedRequest 方法,当认证检查失败的时候调用。

实现自定义认证失败策略

默认的处理认证失败的策略是定向到用户登录页面。我们不总希望如此。比如,如果使用ajax,发送重定向肯能会让用户在页面中间看到一个登录页面。幸运的是,我们可以重写AuthorizeAttribute类的HandleUnauthorizedRequest方法来创建一个自定义策略。

下面的例子实现了自定义认证失败的策略:

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);

}

}

    }

}

当Filter发现是ajax请求,就会按照JSON数据的方式响应。正常的请求还是由基类中默认的策略处理,ajax客户端必须也要写响应的代码响应。

使用Exception Filters

Exception filters只有在运行action方法抛出异常的时候才会运行。这个异常可能从以下几个地方抛出:

其他类型的filter(认证,action,result filter)

action方法自身

当action结果运行后。

创建Exception Filter

Exception filters必须实现IExceptionFilter接口,接口如下:

namespace System.Web.Mvc {

public interface IExceptionFilter {

void OnException(ExceptionContext filterContext);

}

}

OnException方法在抛出异常的时候调用。参数是ExceptionContext。这个类和认证filter的参数类似,也是继承自ControllerContext类。所以你可以获得request信息,并且定义一下额外的指定的filter属性,这些属性如下表:

a5bc9f8cbb6f4ffe871b823833ab60ea[4]

exception filter的Result属性是告诉MVC Framework要做什么。两个主要的用处是记录异常和显示恰当的消息给用户。下例显示了一个演示,当特殊的未处理异常发生时,就会跳转到一个指定的错误页面

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;

            }

    }

}

}

filter回应NullReferenceException实例,而且仅当没有其他exception filter指示处理了异常才发生。我们把用户重定向到一个错误页面,如下代码

...

[MyException]

public ActionResult Index() {

...

如果Index action方法抛出异常,是一个NullReferenceException,而且没有其他 exception filter处理这一个异常,那么我们的filter将重定向到SpecialErrorPage.html。

使用内建的Exception Filter

HandleErrorAttribute是 IExceptionFilter 接口一个实现,它使得创建exception filter更简单。你可以通过下面的属性指定异常和view和layout的名字,从而指定一个异常,如下所示:

7FAF7739799946CA8380DF2350AC34AC[4]

当一个未处理的类型异常出现是,filter会把HTTP result code设置为500,意思是服务器错误,同时显示用户通过View属性指定的view(通过Master使用layout),下面的例子展示了如何使用HandleErrorAttribute filter

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

public ActionResult Index() {

...

上述例子,我们对NullReferenceException类型感兴趣,希望出现异常时显示SpecialErrorView

注意,The HandleErrorAttribute filter 仅在Web.config中自定义error是enabled的情况下工作。比如,在<system.web>节点增加节点 <customErrors mode="On" />。默认的自定义error模式是RemoteOnly,意味着在开发期间,HandleErrorAttribute将不拦截异常,但是当你部署产品服务器,并且从其他电脑上获得请求,HandleErrorAttribute就会采取行动了,要想了解终端用户所看到的,必须确保把自定义error mode设置为On。

当呈现一个view时,HandleErrorAttribute filter传递一个HandleErrorInfo view model对象,你可以在消息中包含异常的细节,展示给用户。如下例子,当现实错误信息时,使用View Model对象:

@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>

效果图如下:

clipboard[10]

使用Action和Result Filter

Action和Result Filter是常用的filter,可以用于任何目的。它们都遵循一个公共模式。内建的类创建filter,IActionFilter的类型,实现全部的接口。接口如下:

namespace System.Web.Mvc {

public interface IActionFilter {

void OnActionExecuting(ActionExecutingContext filterContext);

void OnActionExecuted(ActionExecutedContext filterContext);

    }

}

此接口定义了2个方法,MVC Framework 在action方法调用之前,先调用OnActionExecutin方法。在action方法调用之后,调用OnActionExecuted方法。

实现OnActionExecuting方法

The OnActionExecuting 方法在action方法在action方法调用之前先调用。你可以通过这个来检查请求,选择取消请求,修改请求,或者启动另一个活动跨越action的调用。此方法的参数是一个ActionExecutingContext ,是ControllerContext的子类,有2个同样的属性,和你在其他context对象中看到的一样。详细描述如下表:

60D53A0390644092A324A79F4EF57284[4]

你可以有选择的通过设置parameter的result属性来取消请求,如下演示代码:

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

}

}

}

此例中,使用OnActionExecuting方法检查请求是否是SSL的,如果不是,返回404-Not Found响应给用户。

注意,如上例所示,你不需要将IActionFilter接口中的方法都实现,如果你不需要加任何逻辑,就置空。消息不要抛出NotImplementedException异常,因为如果你这样做,exception filter就会被执行。

实现OnActionExecuted方法

你也可以使用filter去执行一些任务,跨越执行action方法。下面是一个简单的例子计算action方法执行的时间开销。

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));

        }

}

}

}

此例中,我们使用OnActionExecuting方法,启动一个计数器。当action方法完成后,OnActionExecuted方法调用。然后我们停止计时器,写出响应,输出所花的时间。结果图如下:

clipboard[11]

传递给OnActionExecuted方法的参数是一个ActionExecutedContext对象。此类定义了一些额外的属性,如下表。Exception属性返回任何action方法抛出的异常,ExceptionHandled属性指示了是否有其他filter处理了它。

4E577CF0E29040CBAFAC6832552CE036[4]

属性Canceled,如果其他filter cancel了一个请求(通过设置result属性的值),那么返回true。但我们的OnActionExecuted方法仍然调用,只有这样我们才能是否使用的资源。

实现Result Filter

Action filters和result filters有一些共同点. Result filters对于action results 就好比action filters

对于action methods. Result filters 实现IResultFilter 接口, 接口代码如下:

namespace System.Web.Mvc {

public interface IResultFilter {

void OnResultExecuting(ResultExecutingContext filterContext);

void OnResultExecuted(ResultExecutedContext filterContext);

}

}

之前,我们说过action方法如何返回action result。这运行我们分离action的意图和执行。当我们应用一个result filter到action方法上,一旦action方法返回action result,OnResultExecuting方法就被调用,但是在action result执行之前 。OnResultExecuted方法在action result执行后调用。这些方法的参数分别是ResultExecutingContext 和ResultExecutedContext 对象,它们和action filter的那部分非常相似。它们有相同的属性,相同的效果。下例代码展示了一个简单的 result filter的例子。

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));

}

}

}

这个filter计算了执行result所需要的时间。把这个filter附加的action方法上。

...

[ProfileResult]

public ActionResult Index() {

return View();

}

...

现在我们转到action方法,输出的内容如下。

clipboard[12]

注意,从filter中获得的性能信息出现在页面底部。这是因为我们在 action result 执行后——也就是view已经呈现了,才写下我们的消息。我们先前的filte在action result执行之前写到response的,因此它出现在page的顶部。

使用内建的Action和Result Filter类

MVC Framework 包含了一个内建的类,可以创建action 和result filters.

但是不像内建的authorization 和exception filter, 它不提供任何有用的细节. 这个类是ActionFilterAttribute 代码如下:

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) {

}

    }

}

使用这个类的好处就是你不需要实现你用不到的方法。如下例子,展示了继承ActionFilterAttribute的filter,使用action方法和action result执行,合并了我们的性能测量。

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));

}

}

}

ActionFilterAttribute 类实现了IActionFilter 和IResultFilter 接口,意味着MVC Framework把这个继承类看作两种filter类型, 即使并不是所有的方法都被重写。如果我们在上述的例子应用这个filter到action方法,我们的输出就如下:

clipboard[13]