谈谈基于OAuth 2.0的第三方认证 [下篇]

从安全的角度来讲,《中篇》 介绍的Implicit类型的Authorization Grant存在这样的两个问题:其一,授权服务器没有对客户端应用进行认证,因为获取Access Token的请求只提供了客户端应用的ClientID而没有提供其ClientSecret;其二,Access Token是授权服务器单独颁发给客户端应用的,照理说对于其他人(包括拥有被访问资源的授权者)应该是不可见的。Authorization Code类型的Authorization Grant很好地解决了这两个问题。

Authorization Code Authorization Grant授权流程

Authorization Code是最为典型的Authorization Grant,它“完美”地实现了指定的OAuth初衷:资源拥有者可以在向客户端应用提供自身凭证的前提下授权它获取受保护的资源。如右图所 示,Authorization Code类型的Authorization Grant具有完整的“三段式”授权流程,接下来,我们还要针对“集成Windows Live Connect认证 获取当前用户个人信息”这个应用场景来讨论一下Authorization Code类型的Authorization Grant的具体授权流程。

Implicit 类型的Authorization Grant授权的客户端运行于存客户端(浏览器)上下文环境,Authorization Code类型的Authorization Grant则适用于运行于服务器的应用,比如ASP.NET MVC应用的Controller,或者是定义在View中的服务端程序。右图体现的就是在服务器(www.artech.com)运行的客户端应用[1]。

上 面我们已经说过,Authorization Code类型Authorization Grant具有与Kerberos类似的授权方式。如果我们将Access Token看作为了获取受保护资源而“登堂入室”的入场券的话,Authorization Code就是购买这张入场券的“认购权证”。客户端应用需要首先取得Authorization Code,因为它代表了资源拥有者对它的授权,并且是获取Access Token时必须提供的凭证。

客户端应用首先向授权服务器发送一个获取Authorization Code的请求,请求的地址同样为“https://login.live.com/oauth20_authorize.srf”,相应的参数同样以查询字符串的形式提供。与Implicit类型Authorization Grant获取Access Token的请求一样,此时需要提供如下4个完全一样的参数。

  • response_type:表示请求希望获取的对象类型,在此我们希望获取的是Authorization Code,所以这里指定的值为“code”。
  • redirect_uri: 表示授权服务器在获得用户授权并完成对用户的认证之后重定向的地址,Authorization Code就以查询字符串(?code={authorizationcode})的方式附加在该URL后面。客户端应用利用这个地址接收 Authorization Code。
  • client_id: 唯一标识被授权客户端应用的ClientID。
  • scope:表示授权的范围,根据具体需要的权限集而定。

如 果当前用户尚未登录但Windows Live Services,他会被自动重定向到登录页面。在尚未对客户端应用进行授权的情况下,如左图所示的授权页面会显示出来。在取得登录用户的授权之后,授权 服务器会返回一个重定向的响应,而请求提供的redirect_uri参数值直接作为重定向地址。由授权服务器生成的Authorization Code就以查询字符串(?code={authorizationcode})的方式附加在重定向URL的后面。重定向的请求被客户端应用接收后,Authorization Code被提取并保存起来。

接 下来客户端应用会利用得到的Authorization Code向授权服务器获取Access Token,这一般为HTTP-POST请求。作为请求消息主体传递的内容除了作为参数“code”的Authorization Code之外,还包含如下一些必需的参数。

  • client_id: 唯一标识被授权客户端应用的ClientID。
  • client_secret:唯一标识被授权客户端应用的ClientSecret。
  • redirect_uri:之前获取Authorization Code时指定的重定向地址。
  • grant_type:采用的Authorization Grant类型,参数值为“ authorization_code”。

授 权服务器接受到请求之后,除了利用提供的ClientID和ClientSecrete对客户端应用实施验证之外,还会检验之前获取 Authorization Code提供的ClientID和重定向地址是否与本次提供的一致。成功完成检验之后,授权服务器会生成一个Access Token作为响应内容发送给客户端应用。整个响应内容除了Access Token之外,还包含其他一些与之相关的属性。

   1: {
   2: "token_type":"bearer",
   3: "expires_in":3600,
   4: "scope":"wl.signin wl.basic wl.offline_access", 
   5: "access_token":"EwAwAq1DBAAUGCCXc8wU/zFu9QnLdZXy+YnElFkAAZcfA2Qg/7KeYyCTe+jx4bLz8qTAFTV71leUhqb0XEfZlRHdi/YpTUx5raBNbd2TcqmdPT1p6v7NhZHTvwJg7u+nyEosIIB0hjxDPTEkU8nj6HYZ96OP29Vr6rVbWer5tczd5ez7Hm/GOSTcC2c4w7G1hvoh/wpg26Gn/ox5P0dEOiq0FlISC6ADgl9t8feY4SGS0kYOr3MUgH5JMe+ObuoEQavJtxSnXjhr6Vh9Oe8TSAtmsy32f3LMnf/B/8rQHxmGd284OPQlBgH8hy5z0NsrSS6B/4oMFU+oZSYwWaHMjrX2POuM5Wnu3wa6qI3T5a5Zg0qw2KHLy9eMw2a3wz4DZgAACCEjkTQbxjh/AAHTGE/O2koIChcvaQbkt0DQq+lMxtjp0U8rWABcwTz89Vy7zIlz8l9hzAewpiM+W/6Ot1JU9mQKccrVnIKXugVpaqFJbmZ571NPXMI6p7l1uoUR3yPzDBOOKQn5fGeMmyMjZZsMnjQAzm+JxVoLRFnlwZJeTe4BA0x6bAOb/j4T+Nk6I1nTKMuTvFztluWw4oRTMcKNREb35xlbSqiEXnyU214Khc+tiSIeRDMl4mEpHzlSj2iEhzokfIjqaLq1iPW4EQKXYh3i+o44RjZ4effY4jFAe1jtaojRHVZrtq8g6x06LswECPHhH91i2oD8SMzal4DFY6l833XTHbGBoiPiAAA=",      
   6: "refresh_token":"CgnjZWSPqffDqDkt3NeqFHwMKs7xiwpM2gQx0A8WOGbIAPbAXqJAZOB1lhcEV8BOWvZevk5Uo9hUu*lEa8TKXRiw*V8KE8!jhEOMQ9o*uwj*z!O50hN182OueDdJEKX*V8BZhIS0!1K2Ii9*SYREKJQ2UPd0jQaveo9IA0Hz2cAhQCt13KQ!gRKF5bBlzaJh6WJMkgljNXZceurRdyM6QuURzQQUo7DelfW!O74oiVZiH7z*ffd7OKj3sAdIzAphWdlwIXjXxY45uzIMe4dR16jw1aiB0JQdYCqcQSYG*0M233tsVMQjL3cfo0WrRj!w1F!Xob!0zkquhK1JBqdlKWI72Vih!QAWDgYeXf9e*NjO", 
   7: "authentication_token":"eyJhbGciOiJIUzI1NiIsImtpZCI6IjEiLCJ0eXAiOiJKV1QifQ.eyJ2ZXIiOjEsImlzcyI6InVybjp3aW5kb3dzOmxpdmVpZCIsImV4cCI6MTM4NTg2NDM3MSwidWlkIjoiNzY4YjMxYjU3NjFlN2EzMTIzNzk5ZjIzNzFjMDIxOGEiLCJhdWQiOiJ3d3cuYXJ0ZWNoLmNvbSIsInVybjptaWNyb3NvZnQ6YXBwdXJpIjoiYXBwaWQ6Ly8wMDAwMDAwMDQ4MTBDMzU5IiwidXJuOm1pY3Jvc29mdDphcHBpZCI6IjAwMDAwMDAwNDgxMEMzNTkifQ.ETKELC41Nr2CQq9Pwjf_c3lO0egLibnt5K1D4pdOsDs"
   8: }

在客户端应用从响应内容成功提取出Refresh Token之后,可以在任何时候向授权服务器(地址依然是“https://login.live.com /oauth20_authorize.srf”)发送获取新的Access Token的请求。和直通过Authorization Code获取Access Token一样,这通常也是一个HTTP-POST请求,其主体内容携带如下的参数。

  • client_id: 唯一标识被授权客户端应用的ClientID。
  • client_secret:唯一标识被授权客户端应用的ClientSecret。
  • redirect_uri:之前获取Authorization Code时指定的重定向地址。
  • grant_type:采用的Authorization Grant类型,这里自然就是“ refresh_code”。
  • refresh_token:之前利用Authorization Code获取的Access Token。

授权服务器对请求作必要验证后,会将新的Access Token置于响应的主体内容返回给客户端应用。完整地响应内容如下所示,我们不难看出:其中不仅仅包含新的Access Token,还返回了一个新的Refresh Token。

   1: [Authenticate] 
   2: public class DemoController : ApiController
   3: {
   4:     public HttpResponseMessage GetProfile()
   5:     {
   6:         //省略实现
   7:     }
   8: }

如下所示的新AuthenticateAttribute的定义,其中将Access Token添加到响应Cookie中的ExecuteActionFilterAsync方法没有任何变化,我们修改的只是实现自 IAuthenticationFilter接口的两个方法。

   1: public class AuthenticateAttribute : FilterAttribute, IAuthenticationFilter, IActionFilter
   2: {
   3:     public const string CookieName = "AccessToken";
   4:     public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
   5:     {
   6:         //从请求中获取Access Token
   7:         string accessToken;
   8:         if (context.Request.TryGetAccessToken(out accessToken))
   9:         {
  10:             return Task.FromResult<object>(null);
  11:         }
  12:  
  13:         //从请求中获取Authorization Code,并利用它来获取Access Token
  14:         string authorizationCode;
  15:         if (context.Request.TryGetAuthorizationCode(out authorizationCode))
  16:         { 
  17:             string query = string.Format("code={0}", authorizationCode);
  18:             
  19:             //但前请求URI去除“?code={authorizationcode}”部分作为rediect_uri参数
  20:             string callbackUri = context.Request.RequestUri.AbsoluteUri.Replace(query, "").TrimEnd('?');
  21:             using (HttpClient client = new HttpClient())
  22:             {
  23:                 Dictionary<string, string> postData = new Dictionary<string, string>();
  24:                 postData.Add("client_id", "000000004810C359");
  25:                 postData.Add("redirect_uri", callbackUri);
  26:                 postData.Add("client_secret", "37cN-CGV9JPzolcOicYwRGc9VHdgvg6y");
  27:                 postData.Add("code", authorizationCode);
  28:                 postData.Add("grant_type", "authorization_code");
  29:                 HttpContent httpContent = new FormUrlEncodedContent(postData);
  30:                 HttpResponseMessage tokenResponse = client.PostAsync("https://login.live.com/oauth20_token.srf", httpContent).Result;
  31:  
  32:                 //得到Access Token并Attach到请求的Properties字典中
  33:                 if (tokenResponse.IsSuccessStatusCode)
  34:                 {
  35:                     string content = tokenResponse.Content.ReadAsStringAsync().Result;
  36:                     JObject jObject = JObject.Parse(content);
  37:                     accessToken = (string)JObject.Parse(content)["access_token"];
  38:                     context.Request.AttachAccessToken(accessToken);
  39:  
  40:                     return Task.FromResult<object>(null);
  41:                 }
  42:                 else
  43:                 {
  44:                     return Task.FromResult<HttpResponseMessage>(tokenResponse);
  45:                 }
  46:             }
  47:         }
  48:         return Task.FromResult<object>(null);
  49:     }
  50:  
  51:     public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
  52:     {
  53:         string accessToken;
  54:         if (!context.Request.TryGetAccessToken(out accessToken))
  55:         {
  56:             string clientId = "000000004810C359";
  57:             string redirectUri = context.Request.RequestUri.ToString();
  58:             string scope = "wl.signin%20wl.basic";
  59:             string uri = "https://login.live.com/oauth20_authorize.srf";
  60:             uri += "?response_type=code";
  61:             uri += "&redirect_uri={0}&client_id={1}&scope={2}";
  62:             uri = String.Format(url, redirectUri, clientId, scope);
  63:             context.Result = new RedirectResult(new Uri(uri), context.Request);
  64:         }
  65:         return Task.FromResult<object>(null);
  66:     }
  67:  
  68:     public Task<HttpResponseMessage> ExecuteActionFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken,Func<Task<HttpResponseMessage>> continuation)
  69:     {
  70:         HttpResponseMessage response = continuation().Result;
  71:         string accessToken;
  72:         if (actionContext.Request.TryGetAccessToken(out accessToken))
  73:         {
  74:             response.SetAccessToken(actionContext.Request, accessToken);
  75:         }
  76:         return Task.FromResult<HttpResponseMessage>(response);
  77:     }
  78: }

在实现的AuthenticateAsync方法中,我们首选调用自定义的扩展方法TryGetAccessToken试着从当前请求中提取 Access Token。如果Access Token不存在,我们在调用另一个扩展方法TryGetAuthorizationCode试着从当前请求中提取Authorization Code。在成功得到Authorization Code之后,我们将它作为参数调用Windows Live Connect API获取相应的Access Token,并调用扩展方法AttachAccessToken将此Access Token附加到当前请求上。

对于另一个实现的ChallengeAsync方法来说,如果通过调用扩展方法TryGetAccessToken不能从当前请求中得到相应的 Access Token,我们通过为当前HttpAuthenticationChallengeContext的Result属性设置一个 RedirectResult对象实现了重定向。重定向的地址正是一个用于获取Authorization Code的URL(“?response_type=code”),当前请求的URI作为其redirect_uri参数。

如下所示的上面提及的针对HttpRequestMessage类型的3个扩展方法的定义。方法TryGetAuthorizationCode从 请求URL的查询字符串(“code”)中提取Authorization Code;方法AttachAccessToken将Access Token添加到请求的属性字典中;TryGetAccessToken方法则先后从请求的Cookie和属性字典中提取Access Token。

   1: public static class Extensions
   2: {
   3:     //其他成员
   4:     public static bool TryGetAuthorizationCode(this HttpRequestMessage request,  out string authorizationCode)
   5:     {
   6:         authorizationCode = HttpUtility.ParseQueryString(request.RequestUri.Query)["code"];
   7:         return !string.IsNullOrEmpty(authorizationCode);
   8:     }
   9:  
  10:     public static void AttachAccessToken(this HttpRequestMessage request, string accessToken)
  11:     {
  12:         string token;
  13:         if (!request.TryGetAccessToken(out token))
  14:         {
  15:             request.Properties[AuthenticateAttribute.CookieName] = accessToken;
  16:         }
  17:     }
  18:  
  19:     public  static bool TryGetAccessToken(this HttpRequestMessage request, out string accessToken)
  20:     {
  21:         //从请求的Cookie中获取Access Token
  22:         accessToken = null;
  23:         CookieHeaderValue cookieValue = request.Headers.GetCookies(AuthenticateAttribute.CookieName).FirstOrDefault();
  24:         if (null != cookieValue)
  25:         {
  26:             accessToken = cookieValue.Cookies.FirstOrDefault().Value;
  27:             return true;
  28:         }
  29:  
  30:         //获取Attach的Access Token
  31:         object token;
  32:         if( request.Properties.TryGetValue(AuthenticateAttribute.CookieName, out token))
  33:         {
  34:             accessToken = (string)token;
  35:             return true;
  36:         }            
  37:         return false;
  38:     }   
  39: }

当我们利用浏览器第一次调用定义在DemoController的Action方法GetProfile时(假设采用的URI为“https://www.artech.com/webapi/api/demo”),DemoController 上的AuthenticateAttribute特性的AuthenticateAsync方法会率先被执行,但是Access Token和Authorization Code均不存在于当前请求之中,所以并不会执行任何操作。接下来ChallengeAsync方法被执行,浏览器被重定向到Windows Live Connect的授权页面(如果当前用户尚未登录到Windows Live Connect,在此之前会先被重定向到登录页面。如果之前已经完成了授权,授权页面不会再出现)。

在取得了用户授权的情况下,授权服务器会生成一个Authorization Code,并将其作为查询字符串附加到请求提供的重定向地址,然对针对这个URL实施重定向。由于我们设置的重定向地址为 “https://www.artech.com/webapi/api/demo”,所以最终进行重定向的目标地址为“https://www.artech.com/webapi/api/demo?code={authorizationcode}”。

毫无疑问,该地址指向的依然是定义在DemoController中的Action方法GetProfile。在此情况 下,AuthenticateAttribute的AuthenticateAsync方法再次被执行。此时它依然不能从请求中得到Access Token,但是却能得到Authorization Code。于是AuthenticateAttribute利用该Authorization Code调用Windows Live Connect API得到Access Token,并将其添加到请求的属性字典中。

接下来,Action方法GetProfile方法得以执行,它直接从当前请求(实际上是当前请求的属性字典中)中获得Access Token,并利用它调用Windows Live Connect API得到当前登录用户的个人信息。目标Action方法执行结束之后,AuthenticateAttribute又会将Acess Token添加到当前响应的Cookie集合中,所以浏览器在进行Web API调用时会自动将Access Token以Cookie的形式进行发送。

我们提供的这个实例并没有演示如何获取Refresh Token以及在Access Token过期的时候利用它来获取新的Access Token,有兴趣的读者朋友不妨将此功能一并实现在我们自定义的AuthenticateAttribute之中。


[1] 这里介绍的“客户端应用”是针对OAuth 2.0授权角色而言,表示被授权的客户端应用。从运行环境来讲,这个应用可以运行于单纯的客户端上下文(既包括运行于浏览器环境中的Web应用以及在客户 端安装的各种App),也可以运行于服务器(比如Web应用中运行于Web Server的那部分程序)。

谈谈基于OAuth 2.0的第三方认证 [上篇]
谈谈基于OAuth 2.0的第三方认证 [中篇]
谈谈基于OAuth 2.0的第三方认证 [下篇]

 

转载博客,地址:http://www.cnblogs.com/artech/p/oauth-03.html

posted on 2015-12-17 17:45  舒润  阅读(511)  评论(0编辑  收藏  举报

导航