文章出處

很多情況下目標Action方法都要求在一個安全上下文中被執行,這里所謂的安全上下文主要指的是當前請求者是一個經過授權的用戶。授權的本質就是讓用戶在他許可的權限范圍內做他能夠做的事情,授權的前提是請求者是一個經過認證的用戶。質詢-應答(Chanllenge-Response)”是用戶認證采用的一種常用的形式,認證方向被認證方發出質詢以要求其提供用于實施認證的用戶憑證,而被認證方提供相應的憑證以作為對質詢的應答。旨在目標Action方法執行之前實施身分認證的AuthenticationFilter也對這種認證方法提供了支持。

一、IAuthenticationFilter接口

所有的AuthenticationFilter類型均實現了IAuthenticationFilter接口,該接口定義在命名空間“System.Web.Mvc.Filters”下(其他四種過濾器接口都定義在“System.Web.Mvc”命名空間下)。如下面的代碼片斷所示,OnAuthentication和OnAuthenticationChallenge這兩個方法被定義在此接口中,前者用于對請求實施認證,后者則負責將相應的認證質詢發送給請求者。

   1: public interface IAuthenticationFilter
   2: {
   3:     void OnAuthentication(AuthenticationContext filterContext);
   4:     void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext);
   5: }

定義在IAuthenticationFilter接口的兩個方法都將一個上下文對象作為其唯一參數。OnAuthentication方法的這個參數類型為AuthenticationContext,如下面的代碼片斷所示,它是ControllerContext的子類。AuthenticationContext的ActionDescriptor返回的自然是用于描述目標Action方法的ActionDescriptor對象。借助于Principal屬性,我們可以獲取或設置代表當前用戶的Principal對象。如果我們在執行OnAuthentication方法的過程中設置了AuthenticationContext的Result屬性,提供的ActionResult將直接用于響應當前請求。

   1: public class ActionExecutingContext : ControllerContext
   2: {    
   3:     public ActionExecutingContext();
   4:     public ActionExecutingContext(ControllerContext controllerContext, ActionDescriptor actionDescriptor,IDictionary<string, object> actionParameters);
   5:  
   6:     public virtual ActionDescriptor             ActionDescriptor { get; set; }
   7:     public virtual IDictionary<string, object>  ActionParameters { get; set; }
   8:     public ActionResult                         Result { get; set; }
   9: }

OnAuthenticationChallenge方法的參數類型為AuthenticationChallengeContext。如下面的代碼片斷所示,它依然是ControllerContext的子類。它同樣具有一個用于描述目標Action方法的ActionDescriptor屬性,其Result屬性代表的ActionResult對象將用于響應當前請求。

   1: public class ActionExecutedContext : ControllerContext
   2: {    
   3:     public ActionExecutedContext();   
   4:     public ActionExecutedContext(ControllerContext controllerContext, ActionDescriptor actionDescriptor, bool canceled, Exception exception);
   5:  
   6:     public virtual ActionDescriptor     ActionDescriptor { get; set; }
   7:     public virtual bool                 Canceled { get; set; }
   8:     public virtual Exception            Exception { get; set; }
   9:     public bool                         ExceptionHandled { get; set; }
  10:     public ActionResult                 Result { get; set; }
  11: }

二、AuthenticationFilter的執行流程

我們知道身份認證總是對請求處理的第一個步驟,因為只有確定了請求者的真實身份,安全才能得到保障,所以AuthenticationFilter是最先被執行的一類過濾器。所有過濾器的執行都是ActionInvoker來驅動的,ASP.NET MVC在默認情況下采用的ActionInvoker是一個AsyncControllerActionInvoker對象,后者類型派生于ControllerActionInvoker。ControllerActionInvoker針對AuthenticationFilter的執行體現在如下兩個方法(InvokeAuthenticationFilters和InvokeAuthenticationFiltersChallenge)上。

   1: public class ControllerActionInvoker : IActionInvoker
   2: {
   3:     //其他成員
   4:     protected virtual AuthenticationContext InvokeAuthenticationFilters(ControllerContext controllerContext,IList<IAuthenticationFilter> filters, ActionDescriptor actionDescriptor);
   5:     protected virtual AuthenticationChallengeContext InvokeAuthenticationFiltersChallenge(ControllerContext controllerContext, IList<IAuthenticationFilter> filters, ActionDescriptor actionDescriptor, ActionResult result);   
   6: }
如果多個AuthenticationFilter同時被應用到目標Action方法上,ControllerActionInvoker會根據對應Filter的Order/Scope屬性對它們進行排序。隨后ControllerActionInvoker會根據當前ControllerContext、描述目標Action方法的ActionDescriptor對象以及原始的Principal(對應于當前HttpContext的User屬性)創建一個AuthenticationContext對象,并以此作為參數以此調用每個AuthenticationFilter對象的OnAuthentication對象實施認證。

在目標Action方法被執行之后,通過本書第11章“View的呈現”我們知道最終執行的結果會被封裝為一個ActionResult對象。ControllerActionInvoker會利用當前ControllerContext、描述目標Action方法的ActionDescriptor對象和這個ActionResult創建一個AuthenticationChallengeContext對象,并將其作為參數依次調用每個AuthenticationFilter的OnAuthenticationChallenge方法。這個AuthenticationChallengeContext對象的Result屬性最終返回的ActionResult對象將被用來對請求予以響應。

右圖基本反映了整個“AuthenticationFilter鏈”的執行流程,但是如果在執行某個AuthenticationFilter對象的OnAuthenticatio方法時對作為參數的AuthenticationContext對象的Result屬性作了相應的設置,針對整個“AuthenticationFilter鏈”的執行將會立即中止,指定的這個ActionResult對象將用于響應當前請求。如果在執行過程中對AuthenticationContext對象的Principal屬性作了相應的設置,該屬性值將會作為當前HttpContext和當前線程的Principal。

三、實例演示:通過自定義AuthenticationFilter實現Basic認證

在ASP.NET MVC的應用編程接口中,我們找不到IAuthenticationFilter接口的實現者。為了讓大家對這個在ASP.NET MVC 5才引入的過濾器具有更加深刻的認識,我們接下來會通過一個實例來演示如何通過自定義的AuthenticationFilter實現針對Basic方案的認證。不過在這之前,我們有必要對Basic這種基本的認證方法作一個基本的了解。Basic和Digest是兩種典型的HTTP認證方案。對于前者,雖然客戶端提供的認證憑證(用戶名+密碼)僅僅是被Base64編碼而沒有被加密,但是我們可以通過采用HTTPS傳輸利用SSL來解決機密性的問題,所以Basic認證也不失為一種不錯的認證方案。左圖體現了Basic認證的基本流程,可以看出這也是一種典型的采用“質詢-應答”模式的認證方案,整個流程包含如下兩個基本步驟。

  • 客戶端向服務端發送一個HTTP請求,服務端返回一個狀態為“401, Unauthorized”的響應。該響應具有一個“WWW-Authenticate”的報頭標明采用的是Basic認證方案。Basic認證是在一個“領域(Realm)”限定的上下文中進行的,該報頭還可以執行認證的領域,左圖所示的WWW-Authenticate報頭值為:Basic realm="localhost"。
  • · 客戶端向服務端發送一個攜帶基于用戶名/密碼的認證憑證的請求。認證憑證的格式為“{UserName}:{Password}”,并采用Base64編碼(編碼的目的不是為了保護提供的密碼)。這樣一個經過編碼的認證憑證被存放在請求報頭Authorization中,相應的認證方案類型(Basic)依然需要在該報頭中指定,左圖所示的Authorization報頭值為:Basic YcdfaYsss==。服務端接收到請求之后,從Authorization報頭中提取憑證并對其進行解碼,最后采用提取的用戶名和密碼實施認證。認證成功之后,該請求會得到正常的處理,并回復一個正常的響應。

在正式介紹如果定義這個實現Basic認證的AuthenticationFilter之前,我們不妨先來看看使用了這個自定義AuthenticationFilter會產生怎樣的效果。我們在一個ASP.NET MVC應用中定義了如下一個HomeController,定義其中的默認Action方法Index會輸出以三種形式體現的“當前用戶名”。HomeController類型上應用的AuthenticateAttribute特性正是我們自定義的AuthenticationFilter。

   1: [Authenticate]
   2: public class HomeController : Controller
   3: {
   4:     public void Index()
   5:     {
   6:         Response.Write(string.Format("Controller.User: {0}<br/>", this.User.Identity.Name));
   7:         Response.Write(string.Format("HttpContext.User: {0}<br/>", this.ControllerContext.HttpContext.User.Identity.Name));
   8:         Response.Write(string.Format("Thread.CurrentPrincipal: {0}", Thread.CurrentPrincipal.Identity.Name));
   9:     }
  10: }

由于瀏覽器默認提供對Basic認證的支持,所以當我們運行該程序后如下圖所示的登錄對話框會自動彈出,當我們輸入正確的用戶名和密碼(用戶名和密碼直接維護在AuthenticateAttribute上)后,當前登錄用戶名會呈現在瀏覽器上。

這個用于實現Basic認證的AuthenticateAttribute定義如下,簡單起見我們將帳號采用的用戶名和密碼保存在一個靜態字段中。具體的認證實現在實現的OnAuthentication方法中,我們在該方法中調用IsAuthenticated判斷請是否經過認證,并在認證成功的情況下得到代表請求用戶的Principal對象,然對作為參數的AuthenticationContext對象的Principal屬性進行賦值。對于沒有經過認證的請求,我們會調用另一個方法ProcessUnauthenticatedRequest對其進行處理。

   1: public class AuthenticateAttribute:FilterAttribute,IAuthenticationFilter
   2: {
   3:     public const string AuthorizationHeaderName           ="Authorization";
   4:     public const string WwwAuthenticationHeaderName       ="WWW-Authenticate";
   5:     public const string BasicAuthenticationScheme         ="Basic";
   6:     private static Dictionary<string, string> userAccounters;
   7:  
   8:     static AuthenticateAttribute()
   9:     {
  10:         userAccounters = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  11:  
  12:         userAccounters.Add("Foo", "Password");
  13:         userAccounters.Add("Bar", "Password");
  14:         userAccounters.Add("Baz", "Password");
  15:     }
  16:  
  17:     public void OnAuthentication(AuthenticationContext filterContext)
  18:     {
  19:         IPrincipal user;
  20:         if (this.IsAuthenticated(filterContext, out user))
  21:         {
  22:             filterContext.Principal = user;
  23:         }
  24:         else
  25:         {
  26:             this.ProcessUnauthenticatedRequest(filterContext);
  27:         }
  28:     }
  29:  
  30:     protected virtual AuthenticationHeaderValue GetAuthenticationHeaderValue(AuthenticationContext filterContext)
  31:     {
  32:         string rawValue = filterContext.RequestContext.HttpContext.Request.Headers[AuthorizationHeaderName];
  33:         if (string.IsNullOrEmpty(rawValue))
  34:         {
  35:             return null;
  36:         }
  37:         string[] split = rawValue.Split(' ');
  38:         if (split.Length != 2)
  39:         {
  40:             return null;
  41:         }
  42:         return new AuthenticationHeaderValue(split[0], split[1]);
  43:     }
  44:  
  45:     protected virtual bool IsAuthenticated(AuthenticationContext filterContext, out IPrincipal user)
  46:     {
  47:         user = filterContext.Principal;
  48:         if (null != user & user.Identity.IsAuthenticated)
  49:         {
  50:             return true;
  51:         }
  52:  
  53:         AuthenticationHeaderValue token = this.GetAuthenticationHeaderValue(filterContext);
  54:         if (null != token && token.Scheme ==  BasicAuthenticationScheme)
  55:         {
  56:             string credential = Encoding.Default.GetString(Convert.FromBase64String(token.Parameter));
  57:             string[] split = credential.Split(':');
  58:             if (split.Length == 2)
  59:             {
  60:                 string userName = split[0];
  61:                 string password;
  62:                 if (userAccounters.TryGetValue(userName, out password))
  63:                 {
  64:                     if (password == split[1])
  65:                     {
  66:                         GenericIdentity identity = new GenericIdentity(userName);
  67:                         user = new GenericPrincipal(identity, new string[0]);
  68:                         return true;
  69:                     }
  70:                 }
  71:             }
  72:         }
  73:         return false;
  74:     }
  75:  
  76:     protected virtual void ProcessUnauthenticatedRequest(AuthenticationContext filterContext)
  77:     {
  78:         string parameter = string.Format("realm=\"{0}\"", filterContext.RequestContext.HttpContext.Request.Url.DnsSafeHost);
  79:         AuthenticationHeaderValue challenge = new AuthenticationHeaderValue(BasicAuthenticationScheme, parameter);
  80:         filterContext.HttpContext.Response.Headers[WwwAuthenticationHeaderName] = challenge.ToString();
  81:         filterContext.Result = new HttpUnauthorizedResult();
  82:     }
  83:  
  84:     public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext) {}
  85: }

在對請求實施認證的IsAuthenticated方法中,我們會試圖從請求的Authorization報頭中提取安全憑證,并按照Basic憑證的格式解析出用戶名和密碼。只有在用戶名和密碼匹配的情況下,我們認為請求通過認證,并根據解析出來的用戶名創建一個GenericPrincipal對象作為輸出參數user的值。如果請求并為通過認證(它可以是一個匿名請求,或者提供的用戶名與密碼不匹配),方法ProcessUnauthenticatedRequest會被調用。在此情況下,它會對響應的WWW-Authenticate報頭進行相應的設置,并創建一個HttpUnauthorizedResult對象作為AuthenticationContext對象的Result屬性,那么客戶端最終會接收到一個狀態為“401, Unauthorized”的響應。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()