1. 程式人生 > >【 .Net碼農】認識ASP.NET MVC的5種AuthorizationFilter

【 .Net碼農】認識ASP.NET MVC的5種AuthorizationFilter

在總體介紹了篩選器及其提供機制(《深入探討ASP.NET MVC的篩選器》)之後,我們按照執行的先後順序對四種不同的篩選器進行單獨介紹,首先來介紹最先執行的AuthorizationFilter。從命名來看,AuthorizationFilter用於完成授權相關的工作,所以它應該在Action方法被呼叫之前執行才能起到授權的作用。不僅限於授權,如果我們希望目標Action方法被呼叫之前中斷執行的流程“做點什麼”,都可以以AuthorizationFilter的形式來實現。[本文已經同步到《How ASP.NET MVC Works?》中]

目錄 
一、IAuthorizationFilter 
二、AuthorizeAttribute 
三、RequireHttpsAttribute 
四、ValidateInputAttribute 
五、ValidateAntiForgeryTokenAttribute 
六、ChildActionOnlyAttribute

一、IAuthorizationFilter

所有的AuthorizationFilter實現了介面IAuthorizationFilter。如下面的程式碼片斷所示,IAuthorizationFilter定義了一個OnAuthorization方法用於實現授權的操作。作為該方法的引數filterContext是一個表示授權上下文的AuthorizationContext物件, 而AuthorizationContext直接繼承自ControllerContext。

   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”回覆,或者呈現一個錯誤頁面。

二、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 採用的則是“邏輯與”。

   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方法(因為一個使用者只一個使用者名稱)。

   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: {}

三、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並重寫該方法。

四、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則是針對整個請求。

五、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="[email protected]" />
   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”的H