認識ASP.NET MVC的5種AuthorizationFilter
在總體介紹了篩選器及其提供機製(《深入探討ASP.NET MVC的篩選器》)之後,我們按照執行的先後順序對四種不同的篩選器進行單獨介紹,首先來介紹最先執行的AuthorizationFilter。從命名來看,AuthorizationFilter用於完成授權相關的工作,所以它應該在Action方法被調用之前執行才能起到授權的作用。不僅限於授權,如果我們希望目標Action方法被調用之前中斷執行的流程“做點什麼”,都可以以AuthorizationFilter的形式來實現。[本文已經同步到《How ASP.NET MVC Works?》中]
目錄
一、IAuthorizationFilter
二、AuthorizeAttribute
三、RequireHttpsAttribute
四、ValidateInputAttribute
五、ValidateAntiForgeryTokenAttribute
六、ChildActionOnlyAttribute
所有的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。。
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”回複,或者呈現一個錯誤頁麵。
如果我們要求某個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的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應用到同一個類型或者方法上。對於前者,如果當前用於通過了;對於後者來說,意味著需要。以如下兩個方法為例,用戶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這個AuthorizationFilter要求用用戶總是以HTTP請求的方式訪問目標方法。如果當前並不是一個HTTPS請求(通過當前HttpRequest的IsSecureConnection屬性判斷),在HTTP方法為GET的情下,會創建一個RedirectResult對象並用其對AuthorizationContext的Result屬性進行設置,當前請求的URL地址的Scheme替換成HTTPS就成了該RedirectResult的地址。也就是說,如果當前請求地址為://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方法並不是,RequireHttpsAttribute會直接拋出一個InvalidOperationException異常。如上麵的代碼片斷所示,針對非HTTPS請求的處理通過調用受保護的方法HandleNonHttpsRequest來完成,如果我們需要不同的處理,可以繼承RequireHttpsAttribute並重寫該方法。
為了避免用戶在請求中包含一些不合法的內容對網站進行惡意攻擊(比如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:
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的查詢字符串的值設置為為“”。如下圖所示,Action1能夠正常地被調用,而Action2在調用過程中拋出異常 ,並提示請求中包含危險的查詢字符串。
在《ASP.NET MVC Model元數據及其定製:一個重要的接口IMetadataAware》中我們談到可以通過AllowHtmlAttribute特性來定義表示Model元數據的ModelMetadata的RequestValidationEnabled屬性的設置從而忽略對相應屬性數據的驗證,使之可以包含具有HTML標簽的數據。這與ValidateInputAttribute的作用類似,不同的是AllowHtmlAttribute僅僅針對Model對象的默認屬性,而ValidateInputAttribute則是針對整個請求。
具有如下定義的System.Web.Mvc.ValidateAntiForgeryTokenAttribute用於解決一種叫做“跨站請求偽造(: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對應的地址可以表示為https://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="https://foo/blog/updateemailaddress"
7: method="post">
8: <input type="hidden" name="emailAddress" value="malicious@gmail.com" />
9: </form>
10: </body>
11: </html>
假設攻擊者部署該頁麵的地址為https://bar/maliciouspage.html。然後它通過某篇博文中添加一個包含如下鏈接的評論。作為登錄用戶的你點擊該連接後將會間接地調用定義在BlogController的UpdateEmailAddress方法。由於登錄用戶的安全令牌一般以Cookie形式存在,而該Cookie會存在於發送給針對Action方法UpdateEmailAddress的調用請求中,服務器會認為該請求來自被認證用戶,所以最終造成了你的Email地址被惡意修改而不自知。如果攻擊者具有你的用戶名,它可以通過重置密碼,是新的密碼發送到屬於他自己的電子郵箱中。
1: <img src="https://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中調用這些方法是,它們會為我們生成一個所謂“”的字符串,並以此生成一個類型為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異常。
如果我們希望定義在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上下文作為“”保存到表示當前路由信息的RouteData的DataTokens屬性中,對應的Key為“”。如下麵的代碼片斷所示,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: }
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-25 17:04:23