认识ASP.NET MVC的5种AuthorizationFilter

在整体介绍了筛选器及其提供机制(《深刻探讨ASP.NET MVC的筛选器》)以后,咱们按照执行的前后顺序对四种不一样的筛选器进行单独介绍,首先来介绍最早执行的AuthorizationFilter。从命名来看,AuthorizationFilter用于完成受权相关的工做,因此它应该在Action方法被调用以前执行才能起到受权的做用。不只限于受权,若是咱们但愿目标Action方法被调用以前中断执行的流程“作点什么”,均可以以AuthorizationFilter的形式来实现。[本文已经同步到《How ASP.NET MVC Works?》中]javascript

目录 
1、IAuthorizationFilter 
2、AuthorizeAttribute 
3、RequireHttpsAttribute 
4、ValidateInputAttribute 
5、ValidateAntiForgeryTokenAttribute 
6、ChildActionOnlyAttributehtml

1、IAuthorizationFilter

全部的AuthorizationFilter实现了接口IAuthorizationFilter。以下面的代码片段所示,IAuthorizationFilter定义了一个OnAuthorization方法用于实现受权的操做。做为该方法的参数filterContext是一个表示受权上下文的AuthorizationContext对象, 而AuthorizationContext直接继承自ControllerContext。java

   1: public interface IAuthorizationFilter
   2: {    
   3:     void OnAuthorization(AuthorizationContext filterContext);
   4: }
   5:  
   6: public class AuthorizationContext : ControllerContext
   7: {   
   8:     public AuthorizationContext();   
   9:     public AuthorizationContext(ControllerContext controllerContext, ActionDescriptor actionDescriptor);
  10:     
  11:     public virtual ActionDescriptor ActionDescriptor { get; set; }
  12:     public ActionResult Result { get; set; }
  13: }

AuthorizationContext的ActionDescriptor属性表示描述当前执行Action的ActionDescriptor对象,而Result属性返回一个用于在受权阶段呈现的ActionResult。AuthorizationFilter的执行是ActionInvoker进行Action执行的第一项工做,由于后续的工做(Model绑定、Model验证、Action方法执行等)只有在成功受权的基础上才会有意义数组

ActionInvoker在经过执行AuthorizationFilter以前,会先根据当前的Controller上下文和解析出来的用于描述当前Action的ActionDescriptor,并以此建立一个表示受权上下文的AuthorizationContext对象。而后将此AuthorizationContext对象做为参数,按照Filter对象Order和Scope属性决定的顺序执行全部AuthorizationFilter的OnAuthorization。浏览器

在全部的AuthorizationFilter都执行完毕以后,若是指定的AuthorizationContext对象的Result属性表示得ActionResult不为Null,整个Action的执行将会终止,而ActionInvoker将会直接执行该ActionResult。通常来讲,某个AuthorizationFilter在对当前请求实施受权的时候,若是受权失败它能够经过设置传入的AuthorizationContext对象的Result属性响应一个“401,Unauthrized”回复,或者呈现一个错误页面。安全

2、AuthorizeAttribute

若是咱们要求某个Action只能被认证的用户访问,能够在Controller类型或者Action方法上应用具备以下定义的AuthorizeAttribute特性。AuthorizeAttribute还能够具体限制目标Action可被访问的用户或者角色,它的Users和Roles属性用于指定被受权的用户名和角色列表,中间用采用逗号做为分隔符。若是没有显式地对Users和Roles属性进行设置,AuthorizeAttribute在进行受权操做的时候只要求访问者是被认证的用户。服务器

   1: [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=true)]
   2: public class AuthorizeAttribute : FilterAttribute, IAuthorizationFilter
   3: {
   4:     //其余成员   
   5:     public virtual void OnAuthorization(AuthorizationContext filterContext);
   6:     protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext);
   7:   
   8:     public string Roles { get; set; }
   9:     public override object TypeId { get; }
  10:     public string Users { get; set; }
  11: }

若是受权失败(当前访问者是未被受权用户,或者当前用户的用户名或者角色没有在指定的受权用户或者角色列表中),AuthorizeAttribute会建立一个HttpUnauthorizedResult对象,并赋值给AuthorizationContext的Result属性,意味着会响应一个状态为“401,Unauthorized”的回复。若是采用Forms认证,配置的登陆页面会自动被显示。网络

不少会将AuthorizeAttribute对方法的受权与PrincipalPermissionAttribute等同起来,实际上不但它们实现受权的机制不同(后者是经过代码访问安全检验实现对方法调用的受权),它们的受权策略也同样。如下面定义的两个方法为例,应用了PrincipalPermissionAttribute的FooOrAdmin意味着能够被账号为Foo或者具备Admin角色的用户访问,而应用了AuthorizeAttribute特性的方法FooAndAdmin方法则只能被用户Foo访问,并且该用户必须具备Admin角色。也就是说PrincipalPermissionAttribute特性对User和Role的受权逻辑是“逻辑或”,而AuthorizeAttribute 采用的则是“逻辑与”。mvc

   1: [PrincipalPermission( SecurityAction.Demand,Name="Foo", Role="Admin")]
   2: public void FooOrAdmin()
   3: { }
   4:  
   5: [Authorize(Users="Foo", Roles="Admin")]
   6: public void FooAndAdmin()
   7: { }

除此以外,咱们能够将多个PrincipalPermissionAttribute和AuthorizeAttribute应用到同一个类型或者方法上。对于前者,若是当前用于经过了任意一个PrincipalPermissionAttribute特性的受权就有权调用目标方法;对于后者来讲,意味着须要经过全部AuthorizeAttribute特性的受权在具备了调用目标方法的权限。以以下两个方法为例,用户Foo或者Bar能够有权限调用FooOrBar方法,可是没有任何一个用户有权调用CannotCall方法(由于一个用户只一个用户名)。app

   1: [PrincipalPermission( SecurityAction.Demand, Name="Foo")
   2: [PrincipalPermission( SecurityAction.Demand, Name="Bar")]
   3: public void FooOrBar()
   4: { }
   5:  
   6: [Authorize(Users="Foo")]
   7: [Authorize(Users="Bar")]
   8: public void CannotCall()
   9: {}

 

3、RequireHttpsAttribute

从名称也能够看出来来,RequireHttpsAttribute这个AuthorizationFilter要求用用户老是以HTTP请求的方式访问目标方法。若是当前并非一个HTTPS请求(经过当前HttpRequest的IsSecureConnection属性判断),在HTTP方法为GET的情下,会建立一个RedirectResult对象并用其对AuthorizationContext的Result属性进行设置,当前请求的URL地址的Scheme替换成HTTPS就成了该RedirectResult的地址。也就是说,若是当前请求地址为http://www.artech.com/home/index,会自动重定向到https://www.artech.com/home/index

   1: [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=false)]
   2: public class RequireHttpsAttribute : FilterAttribute, IAuthorizationFilter
   3: {   
   4:     protected virtual void HandleNonHttpsRequest(AuthorizationContext filterContext);
   5:     public virtual void OnAuthorization(AuthorizationContext filterContext);
   6: }

若是当前请求的HTTP方法并非GET,RequireHttpsAttribute会直接抛出一个InvalidOperationException异常。如上面的代码片段所示,针对非HTTPS请求的处理经过调用受保护的方法HandleNonHttpsRequest来完成,若是咱们须要不一样的处理,能够继承RequireHttpsAttribute并重写该方法。

4、ValidateInputAttribute

为了不用户在请求中包含一些不合法的内容对网站进行恶意攻击(好比XSS攻击),咱们通常须要对请求的输入进行验证。以下面的代码片段所示,表示HTTP请求的基类HttpRequestBase具备一个ValidateInput方法用于验证请求的输入。实际上这个方法仅仅是在请求上做一下标记而已,在读取相应的请求输入时才根据这些表示决定是否须要进行相应的验证。不过为了便于表达,咱们就将针对该ValidateInput方法的调用说成是针对请求输入的验证。

   1: public abstract class HttpRequestBase
   2: {
   3:     //其余成员
   4:     public virtual void ValidateInput();
   5: }

全部Controller的基类ControllerBase具备以下一个布尔类型的属性ValidateRequest表示是否须要对请求输入进行验证,在默认状况下该属性的默认值为True,意味着针对请求输入的验证默认状况下是开启的。 当ActionInvoker在完成了对全部AuthorizationFilter的执行以后,会根据该属性决定是否会经过调用表示当前请求的HttpRequest对象的ValidateInput方法进行请求输入的验证。

   1: public abstract class ControllerBase : IController
   2: {
   3:     //其余成员
   4:     public bool ValidateRequest { get; set; }
   5: }

也正是因为ActionInvoker针对请求输入验证是在完成了全部AuthorizationFilter的执行以后进行的,因此咱们能够经过自定义AuthorizationFilter的方式来设置当前Controller的ValidateRequest属性进而开启或者关闭针对请求输入的验证。ValidateInputAttribute就是这么作的,咱们能够从以下表示ValidateInputAttribute的定义看出来(构造函数的参数enableValidation表示是否启动针对请求的输入验证)。

   1: [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=false)]
   2: public class ValidateInputAttribute : FilterAttribute, IAuthorizationFilter
   3: {   
   4:     public ValidateInputAttribute(bool enableValidation)
   5:     {
   6:         this.EnableValidation = enableValidation;
   7:     }
   8:  
   9:     public virtual void OnAuthorization(AuthorizationContext filterContext)
  10:     {
  11:         if (filterContext == null)
  12:         {
  13:             throw new ArgumentNullException("filterContext");
  14:         }
  15:         filterContext.Controller.ValidateRequest = this.EnableValidation;
  16:     }
  17:  
  18:     public bool EnableValidation { get; private set; }
  19: }

为了让读者对ValidateInputAttribute这个AuthorizationFilter针对开启和关闭输入验证的做用有一个深入映像,咱们来进行一个简单的实例演示。在经过Visual Studio的ASP.NET MVC项目模板建立的空Web应用中咱们 定义了以下一个HomeController,包含在该Controller中的两个Action方法(Action1和Action2)具备一个字符串类型的参数foo,其中Action1上应用了ValidateInputAttribute特性并将参数设置为False。

   1: public class HomeController : Controller
   2: {
   3:     [ValidateInput(false)]
   4:     public void Action1(string foo, string bar)
   5:     { 
   6:         Response.Write(string.Format("{0}: {1}<br/>", "foo", Server.HtmlEncode(foo)));
   7:         Response.Write(string.Format("{0}: {1}<br/>", "bar", Server.HtmlEncode(bar)));
   8:     }
   9:  
  10:     public void Action2(string foo, string bar)
  11:     { 
  12:         Response.Write(string.Format("{0}: {1}<br/>", "foo", Server.HtmlEncode(foo)));
  13:         Response.Write(string.Format("{0}: {1}<br/>", "bar", Server.HtmlEncode(bar)));
  14:     }
  15: }

咱们直接运行该程序并在浏览器中经过输入相应的地址来访问这两个Action,并以查询字符串的形式指定它们的两个参数。为了检验ASP.NET MVC对请求输入的验证,咱们将表示参数foo的查询字符串的值设置为为“<script></script>”。以下图所示,Action1可以正常地被调用,而Action2在调用过程当中抛出异常 ,并提示请求中包含危险的查询字符串。

image

在《ASP.NET MVC Model元数据及其定制:一个重要的接口IMetadataAware》中咱们谈到能够经过AllowHtmlAttribute特性来定义表示Model元数据的ModelMetadata的RequestValidationEnabled属性的设置从而忽略对相应属性数据的验证,使之能够包含具备HTML标签的数据。这与ValidateInputAttribute的做用相似,不一样的是AllowHtmlAttribute仅仅针对Model对象的默认属性,而ValidateInputAttribute则是针对整个请求。

5、ValidateAntiForgeryTokenAttribute

具备以下定义的System.Web.Mvc.ValidateAntiForgeryTokenAttribute用于解决一种叫作“跨站请求伪造(CSRF:Cross-Site Request Forgery)”。这是一种不一样于XSS(Cross Site Script)的跨站网络攻击,若是说XSS是利用了用户对网站的信任,而CSRF就是利用了站点对认证用户的信任。

   1: [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple=false, Inherited=true)]
   2: public sealed class ValidateAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
   3: {    
   4:     public ValidateAntiForgeryTokenAttribute();
   5:     public void OnAuthorization(AuthorizationContext filterContext);
   6:     public string Salt { get; set; }
   7: }

咱们经过一个简单的例子来对CSRF的原理进行说明。假设咱们经过ASP.NET MVC构建了一个博客应用,做为博主的用户能够发表博文,而通常用于能够对博文发表评论。除此以外,注册用于能够修改本身的Email地址,相关的操做定义在以下所示的BlogController的Action方法UpdateAddress中。

   1: public class BlogController: Controller
   2: {        
   3:     [Authorize]
   4:     [HttpPost]
   5:     public void UpdateEmailAddress(string  emailAddress)
   6:     { 
   7:         //Email地址修改操做
   8:     } 
   9:     //其余成员
  10: }

对于上面定义的UpdateEmailAddress方法,因为应用了AuthorizeAttribute特性,意味着只有认证的用户才能调用它来修改本身提供的Email地址。此外,HttpPostAttribute特性应用在该Action方法上,使咱们只能以POST请求的方式调用它,这无形之中也加强了安全系数。可是这个方法提供的Email修改功能真的安全吗?它真的确保修改后的Email地址真的是登陆用户提供的Email地址吗?

咱们假设BlogController所在的Web应用部署的域名为Foo,那么Action方法UpdateEmailAddress对应的地址能够表示为http://foo/blog/updateemailaddress。如今一个恶意攻击者建立以下一个简单的HTML页面,该页面具备一个指向上面这个地址的表单,而且该表单中具备一个名为emailAddress <input>元素提供属于供给者自身的Email地址。因为注册了window的onload事件,该表单会在页面加载完成以后自动提交。

   1: <html>
   2: <head>
   3:     <script type="text/javascript">
   1:  
   2:         window.onload = function () {
   3:             document.getElementById("updateEmail").submit();
   4:         }
   5:     
</script>
   4: </head>
   5: <body>
   6: <form id="updateEmail" action="http://foo/blog/updateemailaddress" 
   7:     method="post">       
   8:         <input type="hidden" name="emailAddress" value="malicious@gmail.com" />
   9:     </form>
  10: </body>
  11: </html>

假设攻击者部署该页面的地址为http://bar/maliciouspage.html。而后它经过某篇博文中添加一个包含以下连接的评论。做为登陆用户的你点击该链接后将会间接地调用定义在BlogController的UpdateEmailAddress方法。因为登陆用户的安全令牌通常以Cookie形式存在,而该Cookie会存在于发送给针对Action方法UpdateEmailAddress的调用请求中,服务器会认为该请求来自被认证用户,因此最终形成了你的Email地址被恶意修改而不自知。若是攻击者具备你的用户名,它能够经过重置密码,是新的密码发送到属于他本身的电子邮箱中。

   1: <img src="http://bar/maliciouspage.html"/>

这个例子充分说明了CSRF是一种比较隐蔽而且具备很大危害型的网络攻击,促成攻击的缘由在于服务器在针对某个请求执行某个操做的时候并无验证请求的真正来源。对于ASP.NET MVC来讲,若是咱们在执行某个Action方法以前可以确认当前的请求来源的有效性,就能从根本上解决CSRF攻击,而ValidateAntiForgeryTokenAttribute结合HtmlHelper的AntiForgeryToken方法有效地解决了这个问题。

   1: public class HtmlHelper
   2: {
   3:     //其余成员
   4:     public MvcHtmlString AntiForgeryToken();
   5:     public MvcHtmlString AntiForgeryToken(string salt);
   6:     public MvcHtmlString AntiForgeryToken(string salt, string domain, string path);
   7: }

如上面的代码片段所示,HtmlHelper具备三个AntiForgeryToken方法(这里的方式是HtmlHelper的实例方法,不是扩展方法)。当咱们在一个View中调用这些方法是,它们会为咱们生成一个所谓“防伪令牌(Anti-Forgery Token)”的字符串,并以今生成一个类型为Hidden的<input>元素。除此以外,该方法的调用还会根据这个防伪令牌设置一个Cookie。接下来咱们来详细地来讨论这个过程。

上述的这个防伪令牌经过内部类型为AntiForgeryData的对象生成。以下面的代码片段所示,AntiForgeryData具备四个属性,其核心是经过属性Value表示的值。属性UserName和CreationDate表示访问令牌受权的用户名和建立时间。字符串属性Salt是为了加强防伪令牌的安全系数,不一样的Salt值对应着不一样的防伪令牌,不一样的防伪令牌在不一样的地方被使用以免供给者对一个防伪令牌的破解而使整个应用受到全面的攻击。ValidateAntiForgeryTokenAttribute也具备一个同名的属性。

   1: internal sealed class AntiForgeryData
   2: {   
   3:     public string Value { get; set; }
   4:     public string Salt { get; set; }    
   5:     public DateTime CreationDate { get; set; }
   6:     public string Username { get; set; }    
   7: }

当AntiForgeryToken被调用的时候,会先根据当前的请求的应用路径(对应HttpRequest的ApplicationPath属性)计算出表示防伪令牌Cookie的名称,该名称会在经过对应用路径进行Base64编码(编码以前须要进行一些特殊字符的替换工做)生成的字符串前添加“__RequestVerificationToken”前缀。

若是当前请求具备一个同名的Cookie,则直接经过对Cookie的值进行反序列化获得一个AntiForgeryData对象。须要注意的是,这里针对AntiForgeryData进行序列化和反序列化并非一个简单地实现运行时对象到字符串之间的转换,还包含采用MachineKey对AntiForgeryData的四个属性进行加密/解密的过程。若是这样的Cookie不存在,HtmlHelper会随机生成一个长度为16的字节数组,并将对该字节数组进行Base64编码后生成的字符串做为值建立一个AntiForgeryData对象。系统当前时间(UTC)做为该AntiForgeryData对象的建立时间,可是该AntiForgeryData对象的UserName和Salt属性为空。

接下来HtmlHelper会根据以前计算出来的Cookie名称建立一个)HttpCookie对象,而新建立出来的AntiForgeryData对象被序列化后生成的字符串做为该HttpCookie的值。若是咱们在AntiForgeryToken方法调用中设置了表示域和路径的domain和path参数,它们将会做为该HttpCookie对象的Path和Domain属性。最后HtmlHelper将HttpCookie对象设置给当前的HTTP响应。

AntiForgeryToken返回的是一个类型为hidden的<input>元素对应的HTML,该Hidden元素的名称为“__RequestVerificationToken”(即代码访问令牌Cookie名称的前缀)。为了生成该Hidden元素的值,HtmlHelper会根据现有的AntiForgeryData对象(从当前请求获取的或者新建立的)建立一个新的AntiForgeryData对象,两个对象具备相同的CreationDate和Value属性,而当前用户名和指定的Salt参数将会设置给新AntiForgeryData对象的UserName和Salt属性。

   1: @using (Html.BeginForm())
   2: {
   3:   @Html.AntiForgeryToken("647B8734-EFCA-4F51-9D98-36502D13E4E7")
   4:   ...
   5: }

在一个View中咱们经过如上的代码在一个表单中调用HtmlHelper的AntiForgeryToken方法并将一个GUID做为Salt,最终将会生成以下一个名为“__RequestVerificationToken”的Hidden元素。

   1: <form action="..." method="post">
   2: <input name="__RequestVerificationToken" type="hidden" value="yvLaFQ81JVgguKECyF/oQ+pc2/6q0MuLEaF73PvY7pvxaE68lO5qgXZWhfqIk721CBS0SJZjvOjbc7o7GL3SQ3RxIW90no7FcxzR6ohHUYEKdxyfTBuAVjAuoil5miwoY8+6HNoSPbztyhMVvtCsQDtvQfyW1GNa7qvlQSqYxQW7b6nAR2W0OxNi4NgrFEqbMFrD+4CwwAg4PUWpvcQxYA==" />
   3: ...
   4: </form>

对于该View的首次访问(或者对应的Cookie不存在),以下所示的名称为“__RequestVerificationToken_L012Y0FwcDEx”防伪令牌Cookie将会设置,而且是HttpOnly的。

   1: HTTP/1.1 200 OK
   2: Cache-Control: private
   3: ...
   4: Set-Cookie: __RequestVerificationToken_L012Y0FwcDEx=EYPOofprbB0og8vI+Pzr1unY0Ye5BihYJgoIYBqzvZDZ+hcT5QUu+fj2hvFUVTTCFAZdjgCPzxwIGsoNdEyD8nSUbgapk8Xp3+ZD8cxguUrgl0lAdFd4ZGWEYzz0IN58l5saPJpuaChVR4QaMNbilNG4y7xiN2/UCrBF80LmPO4=; path=/; HttpOnly
   5: ...

对于一个请求,若是确保请求提供的表单中具备一个名为“__RequestVerificationToken”的Hidden元素,而且该元素的值与对应的防伪令牌的Cookie值相匹配,就可以确保请求并非由第三方恶意站点发送的,进而防止CSRF攻击。缘由很简单:因为Cookie值是通过加密的,供给者能够获得整个Cookie的内容,可是不能解密得到具体的值(AntiForgeryData的Value属性),因此不可能在提供的表单中也包含一个具备匹配值的Hidden元素。针对防伪令牌的验证就实如今ValidateAntiForgeryTokenAttribute的OnAuthorization方法中。

咱们来具体介绍一下实如今ValidateAntiForgeryTokenAttribute中针对防伪令牌的验证逻辑。首先它根据当前请求的应用路径采用与生成防伪令牌Cookie相同的逻辑计算出Cookie名称。若是对应的Cookie不存在于当前请求中,则直接抛出HttpAntiForgeryException异常;不然获取Cookie值,并反序列化生成一个AntiForgeryData对象。

而后从提交的表单中提取一个名称为“__RequestVerificationToken”的输入元素,若是这样的元素不存在,一样抛出HttpAntiForgeryException异常;不然直接对具体的值进行反序列化生成一个AntiForgeryData对象。最后ValidateAntiForgeryTokenAttribute对这两个AntiForgeryData的Value属性进行比较,以及后者的UserName和Salt属性与当前用户名和自身的Salt属性值进行比较,任何一个不匹配都会抛出HttpAntiForgeryException异常。

6、ChildActionOnlyAttribute

若是咱们但愿定义在Controol中的方法能以子Action的形式在某个View中被调用,这样的调用通常用于生成组成整个View的某个部分的HTML,咱们能够在方法上应用ChildActionOnlyAttribute特性。从以下给出的定义能够看出,ChildActionOnlyAttribute其实是一个AuthorizationFilter,它在重写的OnAuthorization方法中对当前请求进行验证,对于非子Action调用下直接抛出InvalidOperationException异常。

   1: [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple=false, Inherited=true)]
   2: public sealed class ChildActionOnlyAttribute : FilterAttribute, IAuthorizationFilter
   3: {
   4:     public void OnAuthorization(AuthorizationContext filterContext);
   5: }

有的读者可能会问,AuthorizationFilter如何区分当前的请求是基于子Action的调用,而不是通常的Action调用呢?其实很简单,当咱们在调用HtmlHelper的扩展方法Action或者RenderAction的时候会将当前的View上下文做为“父View上下文”保存到表示当前路由信息的RouteData的DataTokens属性中,对应的Key为“ParentActionViewContext”。以下面的代码片段所示,ControllerContext中用于判断是否为子Action请求的IsChildAction属性正式经过该路由信息进行判断的。

   1: public class ControllerContext
   2: {
   3:     //其余成员
   4:     public virtual bool IsChildAction
   5:     {
   6:         get
   7:         {
   8:             RouteData routeData = this.RouteData;
   9:             if (routeData == null)
  10:             {
  11:                 return false;
  12:             }
  13:             return routeData.DataTokens.ContainsKey("ParentActionViewContext");
  14:         }
  15:     }
  16: }
相关文章
相关标签/搜索