Office中國(guó)論壇/Access中國(guó)論壇

 找回密碼
 注冊(cè)

QQ登錄

只需一步,快速開始

返回列表 發(fā)新帖
查看: 3700|回復(fù): 1
打印 上一主題 下一主題

[SharePoint] [ Office 365 開發(fā)系列 ] 身份認(rèn)證

[復(fù)制鏈接]

點(diǎn)擊這里給我發(fā)消息

跳轉(zhuǎn)到指定樓層
1#
發(fā)表于 2018-11-24 16:31:59 | 只看該作者 回帖獎(jiǎng)勵(lì) |倒序?yàn)g覽 |閱讀模式

通常我們?cè)陂_發(fā)一個(gè)應(yīng)用時(shí),需要考慮用戶身份認(rèn)證及授權(quán),Office 365使用AAD(Azure Active Directory)作為其認(rèn)證機(jī)構(gòu),為應(yīng)用程序提供身份認(rèn)證及授權(quán)服務(wù)。因此,在開發(fā)Office 365應(yīng)用前,我們需要了解AAD的認(rèn)證和授權(quán)機(jī)制。

AAD認(rèn)證授權(quán)機(jī)制
當(dāng)前的AAD支持多種身份認(rèn)證標(biāo)準(zhǔn):
  • OpenId Connect
  • OAuth2
  • SAML-P
  • WS-Federation and WS-Trust
  • Graph web api
這幾種身份認(rèn)證標(biāo)準(zhǔn)會(huì)應(yīng)用在不同的場(chǎng)景中,如OAuth2.0應(yīng)用于Office 365應(yīng)用程序接口,SAML-P多應(yīng)用于Office 365的混合部署,我們?cè)陂_發(fā)應(yīng)用的過(guò)程中,最主要是使用OpenID Connect和OAuth2.0.因此,本篇內(nèi)容中只涉及到OpenID和OAuth2.0兩種類型的身份認(rèn)證分析,后續(xù)文章中會(huì)涉及到Office 365的混合部署及令牌交換協(xié)議內(nèi)容。

OAuth2.0是OAuth的最新版本,升級(jí)并簡(jiǎn)化了驗(yàn)證的過(guò)程,在資源授權(quán)方面,OAuth2.0支持多種授予流,Office 365使用授權(quán)代碼授予流和客戶端憑證授予流,兩者適用于不同的應(yīng)用場(chǎng)景,同時(shí)在AAD中配置權(quán)限也進(jìn)行了區(qū)分,稍后會(huì)具體講解。下圖為標(biāo)準(zhǔn)的OAuth2.0處理過(guò)程:





OpenID是目前各大網(wǎng)站普遍支持的開放協(xié)議,OpenID Connect 1.0是基于OAuth2.0設(shè)計(jì)的用戶認(rèn)證標(biāo)準(zhǔn),Azure Active Directory (Azure AD) 中的 OpenID Connect 1.0 允許你使用 OAuth 2.0 協(xié)議進(jìn)行單一登錄。 OAuth 2.0 是一種授權(quán)協(xié)議,但 OpenID Connect 擴(kuò)展了 OAuth 2.0 的身份驗(yàn)證協(xié)議用途。OpenID Connect 協(xié)議(OpenId Connect 1.0)的主要功能是返回 id_token,后者用于對(duì)用戶進(jìn)行身份驗(yàn)證。<span id="mt4" class="sentence SentenceHover" data-guid="42ff71279f609ce44c6a4d52e2f52f5e" data-source=" For more information about OpenID Connect, see the specification, OpenID Connect Core 1.0." style="box-sizing: inherit;">
下圖為OpenID的標(biāo)準(zhǔn)處理過(guò)程:
OpenID的標(biāo)準(zhǔn)過(guò)程需要以下幾步:
1. 客戶端(RP)發(fā)送一個(gè)請(qǐng)求到OpenID的提供商(OP);
2. OP驗(yàn)證用戶,如果用戶尚未授權(quán),則跳轉(zhuǎn)到授權(quán)頁(yè)面;
3. 用戶授權(quán)后,OP會(huì)引導(dǎo)用戶返回到客戶端,并會(huì)攜帶一個(gè)Token和id token;
4. RP使用收到的Token請(qǐng)求用戶其他信息資源;
5. OP返回請(qǐng)求的資源信息

通過(guò)上述的步驟,第三方應(yīng)用(也就是客戶端)不僅可以驗(yàn)證用戶的合法性,同時(shí)可以在用戶授權(quán)的情況下獲取用戶基本信息。在AAD中使用的OpenID Connect 1.0為Auth2.0進(jìn)行了擴(kuò)展,在返回Token的同時(shí),會(huì)返回一個(gè)JWT形式的id_token。AAD中的OpenID終結(jié)點(diǎn)配置信息可通過(guò)訪問(wèn)此鏈接查看:https://login.windows.net/common/.well-known/openid-configuration 。id_token包含用戶的基本信息,作為應(yīng)用的CurrentUser屬性。獲取到Token后,應(yīng)用可以通過(guò)此憑證請(qǐng)求資源,Office 365使用Bearer方式獲取資源,請(qǐng)參閱Bearer Token Usage




授權(quán)代碼流和客戶端憑證授予流
AAD中的授權(quán)代碼授予流使用如下流程:
(此圖引用自msdn)

對(duì)比OAuth2.0的標(biāo)準(zhǔn)流程,授權(quán)代碼流會(huì)以授權(quán)代碼(Code)的方式返回授權(quán)標(biāo)識(shí),用戶通過(guò)使用Code請(qǐng)求資源Token,應(yīng)用程序使用獲取到的Token調(diào)用資源Web API。
當(dāng)我們的Office 365應(yīng)用使用授權(quán)代碼授予流時(shí),需要我們?cè)贏AD中設(shè)置資源代理權(quán)限,設(shè)置過(guò)程如下:
(一)通過(guò)Office 365設(shè)置頁(yè)面進(jìn)入Azure AD:

(二)進(jìn)入AD中的應(yīng)用程序,并找到我們的注冊(cè)應(yīng)用(如何注冊(cè)應(yīng)用請(qǐng)參考),進(jìn)入應(yīng)用的Configure頁(yè)面,如下圖:

(三)設(shè)置資源的Delegated Permissions,如果我們使用過(guò)授權(quán)代碼流來(lái)請(qǐng)求資源,只需設(shè)置Delegation Permissions



AAD中的客戶端憑證授予流使用如下流程:
(此圖引用自msdn)
與標(biāo)準(zhǔn)OAuth2.0流程相比,客戶端憑證授予流不需要用戶授權(quán),而是由應(yīng)用程序直接訪問(wèn)AAD請(qǐng)求token。請(qǐng)注意,如果使用此方式,則應(yīng)用程序?qū)Y源有最大權(quán)限。
當(dāng)我們的Office 365應(yīng)用使用授權(quán)代碼授予流時(shí),需要我們?cè)贏AD中設(shè)置資源的應(yīng)用權(quán)限,與授權(quán)代碼授予流只是配置權(quán)限不同,設(shè)置的是Application Permission,如下圖:




應(yīng)用示例
在實(shí)際應(yīng)用中,我們通常會(huì)使用Owin中間件來(lái)完成用戶身份認(rèn)證,我們使用Office Dev Center中的實(shí)例來(lái)分析。
先來(lái)看如何實(shí)現(xiàn)用戶登錄后驗(yàn)證,我們貼出重要代碼來(lái)分析:
  1. public partial class Startup
  2.     {
  3.         public void ConfigureAuth(IAppBuilder app)
  4.         {
  5.             app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
  6.             app.UseCookieAuthentication(new CookieAuthenticationOptions());
  7.             app.UseOpenIdConnectAuthentication(
  8.                 new OpenIdConnectAuthenticationOptions
  9.                 {
  10.                     ClientId = SettingsHelper.ClientId,
  11.                     Authority = SettingsHelper.Authority,
  12.                     TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
  13.                     {
  14.                         ValidateIssuer = false
  15.                     },
  16.                     Notifications = new OpenIdConnectAuthenticationNotifications()
  17.                     {                       
  18.                         AuthorizationCodeReceived = (context) =>
  19.                         {
  20.                             var code = context.Code;
  21.                             ClientCredential credential = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.AppKey);
  22.                             string tenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
  23.                             String signInUserId = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
  24.                             AuthenticationContext authContext = new AuthenticationContext(string.Format("{0}/{1}", SettingsHelper.AuthorizationUri, tenantID), new ADALTokenCache(signInUserId));
  25.                             AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, SettingsHelper.AADGraphResourceId);
  26.                             return Task.FromResult(0);
  27.                         },
  28.                         RedirectToIdentityProvider = (context) =>
  29.                         {
  30.                             string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
  31.                             context.ProtocolMessage.RedirectUri = appBaseUrl + "/";
  32.                             context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
  33.                             return Task.FromResult(0);
  34.                         },
  35.                         AuthenticationFailed = (context) =>
  36.                         {
  37.                             context.HandleResponse();
  38.                             return Task.FromResult(0);
  39.                         }
  40.                     }
  41.                 });
  42.     }
復(fù)制代碼


上述代碼在項(xiàng)目中的App_Start文件夾下Startup.Auth.cs,是Owin的Server端配置內(nèi)容。Owin中間件是在應(yīng)用啟動(dòng)時(shí)注冊(cè),注冊(cè)方式是掃描跟文件夾下的Startup.cs,存在則使用該配置類注冊(cè)。針對(duì)OWIN的處理機(jī)制,我們?cè)诤罄m(xù)的章節(jié)中單獨(dú)分析OWIN中間件的架構(gòu),當(dāng)前我們主要聚焦在如何使用OpenID及OAuth。在上面的代碼中,有這么一句:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app是Owin中的基礎(chǔ)接口類型,用于內(nèi)部拓展不同的驗(yàn)證機(jī)制,使用IDictionary<string, object> Properties { get; }這樣一個(gè)字典類型存儲(chǔ)我們應(yīng)用程序驗(yàn)證所需的信息。SetDefaultSignInAsAuthenticationType指明Owin默認(rèn)使用的驗(yàn)證方式,為了保持用戶的登錄狀態(tài),我們使用cookie作為默認(rèn)驗(yàn)證方式,當(dāng)cookie未登錄時(shí),Owin繼續(xù)使用下面注冊(cè)的其他方式嘗試驗(yàn)證。
app.UseCookieAuthentication(new CookieAuthenticationOptions());

UseCookieAuthentication是Owin實(shí)現(xiàn)的Cookie驗(yàn)證方式。在Owin的源代碼中,每一種方式都包含基本的處理類:
  • AuthenticationDefaults.cs
  • AuthenticationExtensions.cs
  • AuthenticationHandler.cs
  • AuthenticationMiddleware.cs
  • AuthenticationOptions.cs
此時(shí)我們使用new CookieAuthenticationOptions()初始化Cookie驗(yàn)證默認(rèn)配置。當(dāng)Cookie中無(wú)驗(yàn)證信息時(shí),會(huì)進(jìn)入到app.UseOpenIdConnectAuthentication
在OpenID驗(yàn)證中,配置了如下參數(shù):
  • ClientId:應(yīng)用程序ID,標(biāo)識(shí)我們?cè)贏AD中的應(yīng)用
  • Authority:發(fā)起驗(yàn)證請(qǐng)求的目標(biāo)地址,如當(dāng)前的https://login.windows.net,這里要說(shuō)明一下,根據(jù)我的實(shí)測(cè),https://login.microsoftonline.com也是可以的。
  • TokenValidationParameters:這個(gè)方法是為了驗(yàn)證通過(guò)OpenID驗(yàn)證的用戶是否為本應(yīng)用程序的合法用戶,可根據(jù)業(yè)務(wù)實(shí)際情況編寫自己的驗(yàn)證機(jī)制。
  • Notifications:在OpenID驗(yàn)證并返回后,Owin調(diào)用Notifications方法,也正是我們使用OAuth進(jìn)行用戶授權(quán)的觸發(fā)方法。
下面是一個(gè)TokenValidationParameters參數(shù)的示例:
  1. TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
  2. {                        
  3.    IssuerValidator = (issuer, token) =>
  4.    {
  5.       return DoesIssuerBelongToMyCustomersList(issuer);//DoesIssuerBelongToMyCustomersList方法根據(jù)當(dāng)前登陸人信息判斷是否在用戶列表中,如果不存在,則返回false
  6.    }
  7. }
復(fù)制代碼



本帖子中包含更多資源

您需要 登錄 才可以下載或查看,沒(méi)有帳號(hào)?注冊(cè)

x
分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享分享 分享淘帖 訂閱訂閱

點(diǎn)擊這里給我發(fā)消息

2#
 樓主| 發(fā)表于 2018-11-24 16:32:25 | 只看該作者
接上文


接下來(lái)分析Notifications參數(shù):
  1. AuthorizationCodeReceived = (context) =>
  2. {
  3.     var code = context.Code;
  4.     ClientCredential credential = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.AppKey);
  5.     String signInUserId = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
  6.     AuthenticationContext authContext = new AuthenticationContext("https://login.windows.net/common", new ADALTokenCache(signInUserId));
  7.     AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, SettingsHelper.AADGraphResourceId);
  8.     return Task.FromResult(0);
  9. },
  10. RedirectToIdentityProvider = (context) =>
  11. {
  12.     string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
  13.     context.ProtocolMessage.RedirectUri = appBaseUrl + "/";
  14.     context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
  15.     return Task.FromResult(0);
  16. },
  17. AuthenticationFailed = (context) =>
  18. {
  19.     context.HandleResponse();
  20.     return Task.FromResult(0);
  21. }
復(fù)制代碼

在Notifications參數(shù)方法中定義了3個(gè)委托方法,當(dāng)調(diào)用OpenId驗(yàn)證通過(guò)并返回Code參數(shù)時(shí),Owin調(diào)用AuthorizationCodeReceived,RedirectToIdentityProvider方法用于定義驗(yàn)證通過(guò)后的返回頁(yè)面地址,AuthenticationFailed定義驗(yàn)證失敗后的處理方法。在AuthorizationCodeReceived這個(gè)方法中,我們使用文檔開始提到的AAD授權(quán)代碼流方式為用戶授權(quán)。SettingsHelper.ClientId是應(yīng)用程序ID,是應(yīng)用程序在AAD中的唯一標(biāo)識(shí)。

SettingsHelper.AppKey是應(yīng)用程序中新建的keys(可以使用多個(gè)),新建的方法如下:
進(jìn)入AAD中的應(yīng)用程序管理,添加app key,這個(gè)key是有過(guò)期時(shí)間的,最多2年。這里提醒一下,新建key以后需要保存才能看到key字符串,而且只有第一次能查看,如果忘記了只能重新建一個(gè)。

接著往下面看,context對(duì)象是Owin根據(jù)返回的id_token生成的上下文,這里的signInUserId是用戶在AAD中對(duì)象標(biāo)識(shí)符:
String signInUserId = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;

我們使用signInUserId來(lái)唯一標(biāo)識(shí)用戶的Token緩存對(duì)象,新建一個(gè)AuthenticationContext對(duì)象,這個(gè)對(duì)象是基于ADAL 創(chuàng)建,如有想要了解什么是ADAL,請(qǐng)參閱The New Token Cache in ADAL v2。創(chuàng)建對(duì)象的同時(shí),我們將對(duì)象ADALTokenCache作為TokenCache傳入對(duì)象,ADALTokenCache是我們自定義用來(lái)緩存用戶Token的類,如下:
  1. public class ADALTokenCache : TokenCache
  2.     {
  3.         string User;
  4.         UserTokenCache Cache;

  5.         // constructor
  6.         public ADALTokenCache(string user)
  7.         {
  8.             // associate the cache to the current user of the web app
  9.             User = user;
  10.             this.AfterAccess = AfterAccessNotification;
  11.             this.BeforeAccess = BeforeAccessNotification;
  12.             this.BeforeWrite = BeforeWriteNotification;

  13.             using (ApplicationDbContext db = new ApplicationDbContext())
  14.             {
  15.                 Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == User);
  16.             }

  17.             this.Deserialize((Cache == null) ? null : Cache.cacheBits);
  18.         }

  19.         public override void Clear()
  20.         {
  21.             base.Clear();
  22.             using (ApplicationDbContext db = new ApplicationDbContext())
  23.             {
  24.                 Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == User);
  25.                 if (Cache != null)
  26.                     db.UserTokenCacheList.Remove(Cache);
  27.                 db.SaveChanges();
  28.             }
  29.         }

  30.         void BeforeAccessNotification(TokenCacheNotificationArgs args)
  31.         {
  32.             using (ApplicationDbContext db = new ApplicationDbContext())
  33.             {
  34.                 if (Cache == null)
  35.                 {
  36.                     // first time access
  37.                     Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == User);
  38.                 }
  39.                 else
  40.                 {   // retrieve last write from the DB
  41.                     var status = from e in db.UserTokenCacheList
  42.                                  where (e.webUserUniqueId == User)
  43.                                  select new
  44.                                  {
  45.                                      LastWrite = e.LastWrite
  46.                                  };
  47.                     // if the in-memory copy is older than the persistent copy
  48.                     if (status != null && status.Count() > 0 && status.First().LastWrite > Cache.LastWrite)
  49.                     {
  50.                         Cache = db.UserTokenCacheList.FirstOrDefault(c => c.webUserUniqueId == User);
  51.                     }
  52.                 }
  53.             }
  54.             this.Deserialize((Cache == null) ? null : Cache.cacheBits);
  55.         }

  56.         // Notification raised after ADAL accessed the cache.
  57.         // If the HasStateChanged flag is set, ADAL changed the content of the cache
  58.         void AfterAccessNotification(TokenCacheNotificationArgs args)
  59.         {
  60.             if (this.HasStateChanged)
  61.             {
  62.                 using (ApplicationDbContext db = new ApplicationDbContext())
  63.                 {
  64.                     if (Cache == null || Cache.UserTokenCacheId == 0)
  65.                     {
  66.                         Cache = new UserTokenCache
  67.                         {
  68.                             webUserUniqueId = User,
  69.                             cacheBits = this.Serialize(),
  70.                             LastWrite = DateTime.Now
  71.                         };
  72.                     }
  73.                     else
  74.                     {
  75.                         Cache.cacheBits = this.Serialize();
  76.                         Cache.LastWrite = DateTime.Now;
  77.                     }
  78.                     db.Entry(Cache).State = Cache.UserTokenCacheId == 0 ? EntityState.Added : EntityState.Modified;
  79.                     db.SaveChanges();
  80.                 }
  81.                 this.HasStateChanged = false;
  82.             }
  83.         }

  84.         void BeforeWriteNotification(TokenCacheNotificationArgs args)
  85.         {
  86.             // if you want to ensure that no concurrent write take place, use this notification to place a lock on the entry
  87.         }
  88.     }
復(fù)制代碼



這里我修改了一些代碼,示例中的代碼是用戶每次獲取新資源的Token時(shí)新增一條Cache數(shù)據(jù),為了多用戶訪問(wèn),我將緩存機(jī)制改為每個(gè)用戶對(duì)應(yīng)一條Cache數(shù)據(jù)。
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, SettingsHelper.AADGraphResourceId);

AcquireTokenByAuthorizationCode是ADAL幫我們定義好的授權(quán)代碼流方法,用于通過(guò)code獲取token,同時(shí)我們指定請(qǐng)求SettingsHelper.AADGraphResourceId(AAD Graph web resource)資源,這樣可以驗(yàn)證我們的應(yīng)用是否有對(duì)應(yīng)資源的訪問(wèn)權(quán)限。當(dāng)然,這個(gè)參數(shù)是可選的。

驗(yàn)證邏輯圖如下:




結(jié)束語(yǔ)
Office 365開發(fā)系列的身份認(rèn)證就到這里了,如有不明白的地方,請(qǐng)?jiān)谠u(píng)論中提出。后續(xù)章節(jié)我們會(huì)繼續(xù)深入了解OWIN及ADAL的機(jī)制,希望大家繼續(xù)關(guān)注。



本文轉(zhuǎn)載自博客園:任澤華Ryan《[ Office 365 開發(fā)系列 ] 身份認(rèn)證》




您需要登錄后才可以回帖 登錄 | 注冊(cè)

本版積分規(guī)則

QQ|站長(zhǎng)郵箱|小黑屋|手機(jī)版|Office中國(guó)/Access中國(guó) ( 粵ICP備10043721號(hào)-1 )  

GMT+8, 2025-7-17 00:38 , Processed in 0.102494 second(s), 26 queries .

Powered by Discuz! X3.3

© 2001-2017 Comsenz Inc.

快速回復(fù) 返回頂部 返回列表