閱讀131 返回首頁    go 技術社區[雲棲]


談談基於OAuth 2.0的第三方認證 [中篇]

雖然我們在《上篇》分別討論了4種預定義的Authorization Grant類型以及它們各自的適用場景的獲取Access Token的方式,我想很多之前沒有接觸過OAuth 2.0的讀者朋友們依然會有“不值所雲” 之感,所以在介紹的內容中,我們將采用實例演示的方式對Implicit和Authorization Code這兩種常用的Authorization Grant作深入介紹。本章著重介紹Implicit Authorization Grant。

Implicit Authorization Grant授權流程

假設我們的客戶端應用集成了Windows Live Connect API認證服務,並且在成功取得用戶授權並得到Access Token之後調用相應的Web API獲取當前登錄用戶的個人信息。一般來說,Implicit類型的Authorization Grant大都被將瀏覽器作為執行上下文的客戶端應用采用,換句話說,這樣的客戶端就是在瀏覽器中執行的JavaScript程序。下圖體現了這樣一個采用Implicit類型的Authorization Grant的客戶端應用取得授權、得到Access Token並最終獲取到受保護資源(登錄用戶個人信息)的完整流程。

20083441-a4f9be6c332245f89ee79d2087f2d3e

如右圖所示,用戶會先被客戶端應用重定向到授權服務器(login.live.com),具體的地址為“https://login.live.com/oauth20_authorize.srf”。相關的輸入參數通過查詢字符串的形式,必須提供的參數包含在如下的列表中。

  • response_type: 表示請求希望獲取的對象類型,在此我們希望獲取的是Access Token,所以這裏指定的值為“token”。
  • redirect_uri: 表示授權服務器在獲得用戶授權並完成對用戶的認證之後重定向的地址,Access Token就以Hash(#)的方式附加在該URL後麵。客戶端應用利用這個地址接收Access Token。
  • client_id: 唯一標識被授權客戶端應用的ClientID。
  • scope: 表示授權的範圍,如果采用“wl.signin”意味著允許用戶從客戶端應用直接登錄到Live Services,如果Scope為“wl.basic”則表示運行客戶端應用獲取聯係人信息。如果讀者朋友希望了解Windows Live Connect具體支持那些Scope,可以查閱Windows Live Connect API的官方文檔。

如果當前用戶尚未登錄到Windows Live Services,登錄窗口將會出現,當用戶輸入正確Windows Live帳號和密碼並成功通過認證之後,瀏覽器其上會出現如下圖所示的授權頁麵,具體需要授予的權限集取決於上麵介紹的Scope參數。我們點擊“Yes”按鈕完成授權,成功授權之後,這個的授權頁麵在後續的請求中將不會再出現。

20082459-ea632fe93e1c48df93001783666fb02

授權服務器在獲取用戶的授權之後,會生成一個Access Token。接下來,它會提取請求中指定的重定向地址(即redirect_uri參數),然後將生成的Access Token以Hash(#)的形式附加在該地址後麵,最終針對這個攜帶有Access Token的新地址返回一個重定向的響應。如第一張圖所示,我們采用的重定向地址為“https://www.myapp.com/capturetoken”,那麼最終瀏覽器將會重定向到地址“https://www.myapp.com/capturetoken#acess_token={accesstoken}”上。

這個重定向地址對應著客戶端應用需要獲取授權資源的頁麵,該頁麵可以直接從代表當前地址的URL中獲得Access Token,並利用它來獲取目標資源。對於我們的例子來說,它需要獲取當前Windows Live帳號的基本信息,請求的地址為“https://apis.live.net/v5.0/me”,Access Token以查詢字符串的形式(“?access_token={accesstoken}”)提供給資源服務器,後者據此驗證請求的合法性並在驗證成功的情況下將當前用戶的基本信息以JSON的形式返回給客戶端應用。

實例演示:創建采用Implicit Authorization Grant的Web API應用

接下來我們創建一個ASP.NET Web API程序來實現上麵這個應用場景。我們首先需要按照《上篇》介紹的流程為該應用注冊一個ClientID,如果我們已經在Windows Live Connect上創建了一個應用,我們可以直接使用該應用的ClientID。

假設我們在Windows Live Connect創建了一個采用“https://www.artech.com”作為域名的應用,我們需要利用hosts文件(“%windir%\System32\drivers\etc\hosts”)將此域名映射為本機的IP地址(127.0.0.1),具體的映射腳本如下所示。除此之外,由於我們采用HTTPS並且采用本地IIS作為宿主,所以我們需要為Web API應用所在的站點添加一個HTTPS綁定。

   1: 127.0.0.1 www.artech.com

在具體介紹認證實現原理之前,我們不妨先來演示一下最終達到的效果。我們在ASP.NET Web API應用中定義了如下一個繼承自ApiController的DemoController,它具有唯一一個用於獲取當前登錄用戶個人基本信息的Action方法GetProfile。在該方法中,它通過我們定義的擴展方法TryGetAccessToken從當前請求中提取Access Token,然後利用它調用Windows Live Connect提供的Web API(https://apis.live.net/v5.0/me)。

   1: [Authenticate()] 
   2: public class DemoController : ApiController
   3: {
   4:     public HttpResponseMessage GetProfile()
   5:     {
   6:         string accessToken;
   7:         if (this.Request.TryGetAccessToken(out accessToken))
   8:         {
   9:             using (HttpClient client = new HttpClient())
  10:             {
  11:                 string address = string.Format("https://apis.live.net/v5.0/me?access_token={0}", accessToken);
  12:                 return client.GetAsync(address).Result;
  13:             }
  14:         }
  15:         return new HttpResponseMessage(HttpStatusCode.BadRequest) { ReasonPhrase = "No access token" };
  16:     }
  17: }

集成Windows Live Connect認證的實現最終是通過應用在DemoController類型上的AuthenticateAttribute特性來完成的,這是一個AuthenticationFilter,作為參數的URL指向一個用於獲取和轉發Access Token的Web頁麵。現在我們直接利用瀏覽器來調用定義在DemoController中的Action方法GetProfile,如果當前用戶尚未登錄到Windows Live,瀏覽器會自動重定向到Windows Live的登錄界麵。當我們輸入正確Windows Live帳號和密碼後,當前用戶的基本信息以JSON格式顯示在瀏覽器上(如果尚未對該應用進行授權,如上圖所示的頁麵會呈現出來),具體的效果如下圖所示。

20082503-0a92f01256a748d897745647cbb6f3b

應用在DemoController上的AuthenticateAttribute特性完成了針對授權頁麵的重定向和Access Token的請求和接收。除此之外,為了讓瀏覽器能夠在第一次認證之後能夠自動地發送Access Token,我們利用AuthenticateAttribute將Access Token寫入了Cookie之中,這與Forms認證比較類似。不過就安全的角度來講,利用Cookie攜帶安全令牌會引起一種被稱為“跨站請求偽造(CSRF:Cross-Site Request Forgery)”的安全問題,所以通過HTTP報頭來作為安全令牌的載體是更安全的做法。

如下所示的代碼片斷體現了整個AuthenticateAttribute特性的定義,我們可以看到它同時實現了IAuthenticationFilter和IActionFilter。字符串常量CookieName表示攜帶Access Token的Cookie名稱,隻讀屬性CaptureTokenUri表示授權服務器發送Access Token采用的重定向地址,它指向一個我們由我們設計的Web頁麵,該頁麵在接受到Access Token之後會自動向目標資源所在的地址發送一個請求,該請求地址以查詢字符串的形式攜帶此Access Token。(之所以我們需要利用一個Web頁麵在客戶端(瀏覽器)接收並重發Access Token,是因為授權服務器將返回的Access Token至於重定向URI的Hash(#)部分,所以在服務端是獲取不到的,隻能在客戶端來收集。這個Web頁麵的目的在在於在客戶端獲取的Access Token並發送到服務端。)

   1: [AttributeUsage(AttributeTargets.Class| AttributeTargets.Method)]
   2: public class AuthenticateAttribute : FilterAttribute, IAuthenticationFilter, IActionFilter
   3: {
   4:     public const string CookieName = "AccessToken";
   5:     public string     CaptureTokenUri { get; private set; }
   6:  
   7:     public AuthenticateAttribute(string captureTokenUri)
   8:     {
   9:         this.CaptureTokenUri = captureTokenUri;
  10:     }
  11:  
  12:     public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
  13:     {
  14:         return Task.FromResult<object>(null);
  15:     }
  16:  
  17:     public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
  18:     {
  19:         string accessToken;
  20:         if (!context.Request.TryGetAccessToken(out accessToken))
  21:         {
  22:             string clientId = "000000004810C359";
  23:             string redirectUri = string.Format("{0}?requestUri={1}", this.CaptureTokenUri, context.Request.RequestUri);
  24:             string scope = "wl.signin%20wl.basic";
  25:  
  26:             string uri = "https://login.live.com/oauth20_authorize.srf";
  27:             uri += "?response_type=token";
  28:             uri += "&redirect_uri={0}&client_id={1}&scope={2}";
  29:             uri = String.Format(uri, redirectUri, clientId, scope);
  30:             context.Result = new RedirectResult(new Uri(uri), context.Request);
  31:         }
  32:         return Task.FromResult<object>(null);
  33:     }
  34:  
  35:     public Task<HttpResponseMessage> ExecuteActionFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
  36:     {
  37:         HttpResponseMessage response = continuation().Result;
  38:         string accessToken;
  39:         if (actionContext.Request.TryGetAccessToken(out accessToken))
  40:         {
  41:             response.SetAccessToken(actionContext.Request, accessToken);
  42:         }
  43:         return Task.FromResult<HttpResponseMessage>(response);
  44:     }
  45: }

在實現的ChallengeAsync方法(該方法在認證過程中向客戶端發送“質詢”響應)中,我們利用自定義的擴展方法TryGetAccessToken試著從當前請求中獲取攜帶的Access Token。如果這樣的Access Token不存在,我們通過為HttpAuthenticationChallengeContext的Result屬性設置一個RedirectResult對象實現針對Windows Live Connect授權頁麵的重定向,相關的參數(respone-type、redirect_uri、client_id和scope)以查詢字符串的形式提供。

值得一提的作為重定向地址的參數redirect_uri,我們會將當前請求的地址作為查詢字符串(名稱為“requestUri”)附加到CaptureTokenUri上得到的URI作為該參數的值,當前請求的地址正式Web頁麵發送Access Token的目標地址。

另一個實現的ExecuteActionFilterAsync方法複雜將Access Token寫入響應Cookie之中,具體的操作實現在我們自定義的擴展方法SetAccessToken中。下麵的代碼片斷給出了兩個擴展方法SetAccessToken和TryGetAccessToken的定義。

   1: public static class Extensions
   2: {
   3:     public  static bool TryGetAccessToken(this HttpRequestMessage request, out string accessToken)
   4:     {
   5:         //從Cookie中獲取Access Token
   6:         accessToken = null;
   7:         CookieHeaderValue cookieValue = request.Headers.GetCookies(AuthenticateAttribute.CookieName).FirstOrDefault();
   8:         if (null != cookieValue)
   9:         {
  10:             accessToken = cookieValue.Cookies.FirstOrDefault().Value;
  11:             return true;
  12:         }
  13:         
  14:         //從查詢字符串中獲取Access Token
  15:         accessToken = HttpUtility.ParseQueryString(request.RequestUri.Query)["access_token"];
  16:         return !string.IsNullOrEmpty(accessToken);
  17:     }
  18:  
  19:     public static void SetAccessToken(this HttpResponseMessage response, HttpRequestMessage request, string accessToken)
  20:     {
  21:         if (request.Headers.GetCookies(AuthenticateAttribute.CookieName).Any())
  22:         {
  23:             return;
  24:         }
  25:         CookieHeaderValue cookie = new CookieHeaderValue(AuthenticateAttribute.CookieName, accessToken)
  26:         {
  27:             HttpOnly = true,
  28:             Path = "/"
  29:         };
  30:         response.Headers.AddCookies(new CookieHeaderValue[] { cookie });
  31:     }
  32: }

在我們演示的實例中,應用在DemoController類型上的AuthenticateAttribute特性的CaptureTokenUri屬性(“https://www.artech.com/webapi/account/capturetoken”)指向定義在AccountController這麼一個Controller(ASP.NET MVC的Controller,不是ASP.NET Web API的HttpController)的Action方法CaptureToken,具體定義如下所示。

   1: public class AccountController : Controller
   2: {
   3:     public ActionResult CaptureToken(string requestUri)
   4:     {
   5:         ViewBag.RequestUri = requestUri;
   6:         return View();
   7:     }
   8: }

由於AuthenticateAttribute在調用Windows Live Connect的API獲取Access Token所指定的重定向地址具有一個名為“requestUri”的查詢字符串,其值正好是調用Web API的地址,該地址會自動綁定到Action方法CaptureToken的requestUri參數上。如果上麵的代碼片斷所示,該方法會將該地址以ViewBag的形式傳遞到呈現的View之中。

   1: <html>
   2:     <head>
   3:         <script src="@Url.Content("~/scripts/jquery-1.10.2.js")"></script>
   4:         <script type="text/javascript">
   5:             $(document).ready(function () {
   6:                 var redirectUri = '@ViewBag.RequestUri';
   7:                 if (redirectUrl.indexOf('?') >= 0) {
   8:                     redirectUrl += "&" + location.hash.slice(1)
   9:                 }
  10:                 else {
  11:                     redirectUrl += "?" + location.hash.slice(1)
  12:                 }
  13:                 location.href = redirectUri;
  14:             });
  15:         </script>
  16:     </head>
  17: </html>

上麵的代碼片斷代表Action方法CaptureToken對應View的定義。在該View中,我們從當前地址的Hash(#)部分得到Access Token,並將其作為查詢字符串附加到從ViewBag中得到的資源訪問地址上,並通過設置location的href屬性的方式攜帶Access Token對Web API再次發起調用。


談談基於OAuth 2.0的第三方認證 [上篇]
談談基於OAuth 2.0的第三方認證 [中篇]
談談基於OAuth 2.0的第三方認證 [下篇]


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-25 15:04:48

  上一篇:go  談談基於OAuth 2.0的第三方認證 [上篇]
  下一篇:go  談談基於OAuth 2.0的第三方認證 [下篇]