在整體介紹了篩選器及其提供機制(《深刻探討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
全部的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」回覆,或者呈現一個錯誤頁面。安全
若是咱們要求某個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: {}
從名稱也能夠看出來來,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並重寫該方法。
爲了不用戶在請求中包含一些不合法的內容對網站進行惡意攻擊(好比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在調用過程當中拋出異常 ,並提示請求中包含危險的查詢字符串。
在《ASP.NET MVC Model元數據及其定製:一個重要的接口IMetadataAware》中咱們談到能夠經過AllowHtmlAttribute特性來定義表示Model元數據的ModelMetadata的RequestValidationEnabled屬性的設置從而忽略對相應屬性數據的驗證,使之能夠包含具備HTML標籤的數據。這與ValidateInputAttribute的做用相似,不一樣的是AllowHtmlAttribute僅僅針對Model對象的默認屬性,而ValidateInputAttribute則是針對整個請求。
具備以下定義的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異常。
若是咱們但願定義在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: }