ASP.NET MVC3 Action Filters详解(二)
上一篇文章从整体上介绍了ASP.Net MVC Filters的用法,ASP.Net MVC 自带了两个非常有用的Filters —— AuthorizeAttribute和OutputCacheAttribute,这篇文章会详细介绍它们的用法。
AuthorizeAttribute
AuthorizeAttribute这个Filter实现了IAuthorization这个接口,是Asp.Net MVC提供的认证和授权功能的实现,开发人员要实现自己的认证和授权功能,不必从头开始写一个Filter,只需要继承AuthorizeAttribute,然后覆盖它的方法就可以了,这个类本身有许多virtual方法可供我们重写以实现自己的业务逻辑
我们知道,实现IAuthorization接口的Filter会在其他所有Filter和Action方法之前被调用,系统调用IAuthorization.OnAuthorization方法,如果方法中给上下文参数设置了ActionResult,就不会继续调用Action方法,而是直接执行这个ActionResult,返回结果给客户端。
AuthorizeAttribute的OnAuthorization方法内部调用了AuthorizeCore方法,这个方法是实现验证和授权逻辑的地方,如果这个方法返回true,表示授权成功,如果返回false,表示授权失败,会给上下文设置一个HttpUnauthorizedResult,这个ActionResult执行的结果是向浏览器返回一个401状态码(未授权)。
现在来看一看AuthorizeCore方法,我们如果要实现自己的验证授权逻辑也只需要覆盖这个方法即可。
protected virtual bool AuthorizeCore(HttpContextBase httpContext) {
if (httpContext == null) {
throw new ArgumentNullException("httpContext");
}
//第一步,用户认证
IPrincipal user = httpContext.User;
if (!user.Identity.IsAuthenticated) {
return false;
}
//第二步,基于用户名的授权
if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase)) {
return false;
}
//第三步,基于用户角色的授权
if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole)) {
return false;
}
return true;
}
这个方法分三步:
第一步,用户是否已经认证(即是否知道用户是谁)
第二步,用户名是否在允许的用户名列表中(通过AuthorizeAttribute特性的Users属性设置本Action允许的用户名称,多个名称用逗号间隔,如果没有设置这个属性,则忽略这一步)。
第三步,用户所在的角色是否是允许的角色(通过AuthorizeAttribute特性的Roles属性设置本Action允许的角色名称,多个名称用逗号间隔,如果没有设置这个属性,则忽略这一步)。
当以上三步都通过后,AuthorizeCore返回true,否则返回false。
多数情况下我们借助Asp.Net的表单验证机制完成上面的第一步用户认证和第二步基于用户名的授权,我们需要在Web.config配置文件中开启表单验证:
<authentication mode=”Form” />
然后在用户登陆成功时可以调用FormsAuthentication.SetAuthCookie方法将用户的身份信息通过Cookie保存在用户电脑上,该用户的后续请求中httpContext.User.Identity.IsAuthenticated就会返回true,同时httpContext.User.Identity.Name会返回FormsAuthentication.SetAuthCookie方法设置的用户名。
上面代码第三步中通过user.IsInRole方法判断用户是否属于某个角色,这用到了Asp.Net的成员资格API,成员资格API是Asp.Net提供的一整套用户管理架构,我个人认为这套架构过于复杂,配置繁琐,且不易扩展,所以我不建议使用。我建议在实际项目中覆盖AuthorizeCore方法,然后第一步认证、第二步基于用户名授权都不需要改动,到第三步基于角色授权时使用自己编写的逻辑代替原有代码。
OutputCacheAttribute
我们知道在传统的Asp.Net WebForm开发中有页面缓存,只要在页面标记中添加<%@ OutputCache Duration=”xxx” VaryByParam=”xxx” %>就可以对页面缓存,这个标记内部使用Response.Cache实现缓存。它是对页面输出结果html的缓存,因此也叫做输出缓存。
Asp.Net中还有一种缓存,它不是对页面的缓存,而是对任意对象的缓存,它使用HttpContext.Cache属性作为缓存容器,HttpContext.Cache是和HttpContext.Application类似的应用程序全局容器。由于它可以缓存任意对象,因此也叫做对象缓存或数据缓存。
在.net framework 4中新增了System.Runtime.Caching命名空间,它里面的类实现了不依赖于Asp.Net的对象缓存,System.Runtime.Caching.MemoryCache这个类的功能和HttpContext.Cache的功能相似,使用方法也类似,不同的是它不再依赖Asp.Net,可以用在任意应用程序中。
Asp.Net MVC中的OutputCacheAttribute实现了对Action执行结果的缓存,这个标记实现的是一种输出缓存,也就是说是对Action执行结果的html的缓存,而不是对数据(对象)的缓存。这个特性的属性基本上和WebForm的<%@ OutputCache %>指令的属性一致。所以在使用上也和<%@ OutputCache %>指令一致,实际上,它们内部都是使用Response.Cache实现的。
在Asp.Net MVC2中增加了Child Action,即可以在视图中使用Html.Action或Html.RenderAction调用另一个Action并插入执行结果。ChildAction的引入给输出缓存带来了复杂,其中一个问题是对Child Action应用缓存会导致父Action的输出结果也被缓存,当时的解决办法是使OutputCacheAttribute对Child Action失效。后来在Asp.Net MVC3 中对这个问题进行了改进,现在Child Action的内容也可以被单独缓存而不会影响父Action,解决的办法是对父Action使用Response.Cache进行输出缓存,对Child Action的输出结果在一个全局静态的System.Runtime.Caching.MemoryCache实例中进行缓存,这样Child Action和父Action的内容就分别在两个地方缓存,不会相互影响,当然父Action的缓存结果中会包括Child Action的内容,但Child Action的内容又单独保存了一份。
上述解决方法使OutputCacheAttribute的源码变得很复杂,它同时实现了IActionFilter、IResultFilter、IExceptionFilter三个接口。
对于Child Action,在IActionFilter.OnActionExecuting方法中暂时将Child Action的输出导入一个自定义的TextWriter中,然后在IResultFilter.OnResultExecuted方法中从这个TextWriter中取得输出结果在全局的System.Runtime.Caching.MemoryCache实例中进行缓存,再将结果输出给HttpContext.Response。
对于非Child Action,在IResultFilter.OnResultExecuting中开启对Action执行结果的输出缓存(通过一个自定义OutputCachedPage)。
在OutputCacheAttribute的使用上,不必区分是否是Child Action,用法都是一致的。更多细节可以参考OutputCacheAttribute类的源代码。