談談基於OAuth 2.0的第三方認證

funnyok發表於2021-09-09

雖然我們在《上篇》分別討論了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並最終獲取到受保護資源(登入使用者個人資訊)的完整流程。

圖片描述  

如右圖所示,使用者會先被客戶端應用重定向到授權伺服器(login.live.com),具體的地址為“

  • 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”按鈕完成授權,成功授權之後,這個的授權頁面在後續的請求中將不會再出現。

圖片描述  

授權伺服器在獲取使用者的授權之後,會生成一個Access Token。接下來,它會提取請求中指定的重定向地址(即redirect_uri引數),然後將生成的Access Token以Hash(#)的形式附加在該地址後面,最終針對這個攜帶有Access Token的新地址返回一個重定向的響應。如第一張圖所示,我們採用的重定向地址為“{accesstoken}”上。

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

例項演示:建立採用Implicit Authorization Grant的Web API應用

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

假設我們在Windows Live Connect建立了一個採用“%windir%System32driversetchosts”)將此域名對映為本機的IP地址(127.0.0.1),具體的對映指令碼如下所示。除此之外,由於我們採用HTTPS並且採用本地IIS作為宿主,所以我們需要為Web API應用所在的站點新增一個HTTPS繫結。

   1: 127.0.0.1 

在具體介紹認證實現原理之前,我們不妨先來演示一下最終達到的效果。我們在ASP.NET Web API應用中定義瞭如下一個繼承自ApiController的DemoController,它具有唯一一個用於獲取當前登入使用者個人基本資訊的Action方法GetProfile。在該方法中,它透過我們定義的擴充套件方法TryGetAccessToken從當前請求中提取Access Token,然後利用它呼叫Windows Live Connect提供的Web API(

   1: [Authenticate("https:///webapi/account/capturetoken")]

   

   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("{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格式顯示在瀏覽器上(如果尚未對該應用進行授權,如上圖所示的頁面會呈現出來),具體的效果如下圖所示。

圖片描述

應用在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(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 = "";

   

  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(null);

   

  33:     }

   

  34:

   

  35:     public Task ExecuteActionFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func> 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(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:///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: 

   

   2:     

   

   3:         

   

   4:         

   

   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:         

   

  16:     

   

  17: 

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



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3137/viewspace-2805185/,如需轉載,請註明出處,否則將追究法律責任。

相關文章