天天看點

從零搭建一個IdentityServer——資源與通路控制

  IdentityServer作為授權伺服器它的最終目的是用于對資源進行管控,這裡所說的資源有兩種,其一是API資源,實際上也就是OIDC協定中用戶端(RP)所需要通路的一系列受保護的資源(API),授權伺服器通過對終端使用者完成身份驗證後發放相應Token,然後可以使用Token來完成受保護資源的通路。

  另外就是對使用者資源進行管控,簡單來說就是授權伺服器存儲了使用者相關資訊,用戶端應用無需也無權來管理,如有需要可以通過授權伺服器擷取,這樣的好處就是将使用者資訊統一管理,可以保證使用者資料一緻性、安全性也可以減少用戶端程式的開發量。

  随着軟體或者資訊化的不斷發展,現在一個常見的軟體使用場景就是,很多軟體都可以支援第三方賬号登入,登陸時首先會有一個授權登入XXX應用的提示,當使用者同意且登入成功後軟體可以擷取到第三方賬号的相關資訊,如頭像、昵稱等,甚至還可以申請并擷取賬号的手機号碼等隐私資訊,最常見的例子就是微信公衆号/小程式。

  本文的主題就是如何通過IdentityServer4來對資源進行管控,最後實作通路第三方應用程式(用戶端,RP)時授權提示及使用者資訊申請的過程。

  本文内容有:

  • Resource定義
  • Client定義
  • Identity Resource與Asp.net Core Identity內建
    • Profile Service
    • ClaimTypes
    • IdentityServer4與Asp.net Core Identity的內建
  • Asp.net core基于Scope的通路授權
  • IdentityServer4啟用Consent
  • 小結

  借用IdentityServer4官方文檔的一句話“OpenID Connect或OAuth Token服務的最終目的就是控制資源的通路”,而這裡的資源類别有兩種,其一就是API資源,可以把它看成一系列受保護的可遠端調用的内容,甚至可以直接狹義的了解為基于Http協定的Web API。另外就是使用者資訊資源,如使用者昵稱、頭像、手機号碼等等。

  在IdentityServer4中,使用IdentityResource來定義一個使用者資源,一個使用者資源除了有名稱、展示名稱等屬性外還包含一系列的屬性,将這一系列的使用者屬性統稱為ClaimType,舉個例子官網文檔自定義profile資源的例子(注:預設的profile資源包含了name, family_name, given_name, middle_name, nickname等ClaimType資訊,具體參考文檔:https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims):

  

從零搭建一個IdentityServer——資源與通路控制

  從圖中可以看到這個自定義資源設定了名稱、展示名稱及一個ClaimTypes清單,簡單來說就是這個使用者資源包含了使用者名、郵箱和狀态,當用戶端(RP)擁有這個資源的通路權限後,它就可以通過授權伺服器獲得使用者的相關資訊。更多IdentityResource定義參考文檔:https://identityserver4.readthedocs.io/en/release/reference/identity_resource.html

  在IdentityServer4中,使用ApiResource來定義一個API資源,它的基礎結構與使用者資源類似,也是包含名稱、展示名稱,隻是不同的是它擁有一個scope清單,一個scope可以按照字面意思了解,就是這個資源的範圍,這個範圍由人來定義,可大可小,并且scope可以獨立于資源單獨存在,一個應用程式可以隻有一個scope,換句話說就是當使用者擁有這個scope的權限,那麼就可以通路這個應用程式的所有内容,也可以細粒度的一個Api就對應一個Api資源,一個Api資源中包含多個scope,如将這個api的每一個子功能或權限都定義為一個scope。

  下圖為一個ApiResource定義的基本結構,它是針對Api級别定義的,這個資源下面有兩個scope分别對應這個api的完全通路和隻讀通路兩個權限:

從零搭建一個IdentityServer——資源與通路控制

  更多ApiResource定義參考文檔:https://identityserver4.readthedocs.io/en/release/reference/api_resource.html

  Client就是代表之前文章中提到的用戶端(RP)應用程式,那麼定義Client實際上就是應用程式的一些特性及應用程式的功能。

  下圖為一個Client的定義資訊,它包含了Client的Id、名稱、授權方式等,但本文主要關注資源控制,是以主要關注的是Client的AllowedScopes屬性,它包含了所允許通路的使用者資源和Api資源資訊,下圖Client的Scope定義中我們可以看出,該應用程式可以通路使用者的id(OpenId)、使用者基本資訊(Profile)及郵箱,同時定義了該應用程式有api1、api2.read_only兩個api資源:

從零搭建一個IdentityServer——資源與通路控制

  更多Client的定義參考文檔:https://identityserver4.readthedocs.io/en/release/reference/client.html

Identity Resource與Asp.net Core Identity

  前面了解了Identity Resource包含了使用者的基本資訊,而在我們常用的asp.net core應用程式中,使用者資訊都通過Asp.net core Identity進行管理,包括本系列文章也是通過Identity來完成使用者資訊管理的,但是一般情況下Asp.net core Identity通過UserManager等類型來完成使用者資訊管理(主要是指擷取),而現在情況比較特殊IdentityServer4的UserInfo EndPoint是用來擷取使用者資訊的,關鍵問題是使用者資訊存儲仍然通過Asp.net core Identity實作,進而引出一個它們之間如何互相關聯工作的問題。

  關于Identity Resource與Identity元件的關聯主要有以下兩方面内容:

  Profile Service是IdentityServer4中用于提供使用者資訊的服務,在IdentityServer4核心類庫中它定義了一個IProfileService的接口,這個接口定義了兩個無傳回值的方法,分别用于擷取使用者資訊和判斷賬戶是否可用,接口定義如下圖所示。

從零搭建一個IdentityServer——資源與通路控制

  這裡要注意的是因為沒有傳回值,是以實際上兩個方法所需傳回的資料都是通過填充傳入參數來實作資料傳遞,其中使用者資料請求上下文(ProfileDataRequestContext)通過其它相關參數,如使用者id(Subject Id)、請求的claimTypes(RequestedClaimTypes,這個參數的意義在于這個服務不是每次都将使用者的所有資訊都進行傳回,而是隻傳回需要的,如通過UserInfo EndPoint來擷取使用者資訊時,這個參數就會攜帶email、profile等claimTypes,而生成Access Token時還會攜帶如api1、api2.read_only之類的api scope),來擷取使用者資訊,最終将使用者資訊填充到IssuedClaims這個清單中:

從零搭建一個IdentityServer——資源與通路控制

  簡單來說IdentityServer4使用者資訊擷取就依賴于這個接口,想要擷取特定存儲的使用者資訊,那麼根據情況實作該接口即可,那麼我們可以猜測IdentityServer4與Asp.net core Identity的內建實際上是實作了一個基于Asp.net core Identity的ProfileService。

ClaimType

  了解了資料的擷取問題之後,還有一個問題就是資料之間的映射,假設現在有兩個系統,系統A和系統B,系統A中存在一個名為身份證号碼的資料,系統B中存在一個Id Card No.的資料,人們可以很容易知道兩個資料雖然名稱不一樣,但是内容是一樣的,但是計算機不行,我們需要在它們之間建立一個映射關系,建立映射關系之前首先得了解它們對資料的命名規則。

  無論是asp.net core identity還是OIDC的使用者資料,實際上都是用Claim來表示使用者資訊的,這是它們之間的一個共同點,即資料結構一緻,簡單來說隻要名稱能對上那麼就能互相交換資料了,這裡需要引出兩個Claim的定義,其一是.Net的ClaimTypes,它位于System.Security.Claims命名空間下,定義了一個使用者常用的claim type,具體資訊如下圖所示:

從零搭建一個IdentityServer——資源與通路控制

  另外一個是Jwt的ClaimTypes,它的定義可以參考文檔:https://www.iana.org/assignments/jwt/jwt.xhtml,在.Net中可以使用IdentityModel類庫來直接使用相關定義,具體内容如下圖所示:

從零搭建一個IdentityServer——資源與通路控制

  在上面兩張圖檔中分别用紅框标明了ClaimTypes的NameIdentifier、Name和JwtClaimTypes的Subject、Name,兩個值分别對應了使用者的Id和使用者名,可以看出它們的claim名稱(及相同名稱的值)并不一緻。

  如果想要實作資料互通,那麼隻需要将相同意義的Claim進行對應即可。

  我們知道OIDC或者說Oauth2.0中涉及的Token基本使用jwt來作為規範,但是從上面System.Security.Claims命名空間下對ClaimType的定義中可以看到它和jwt的Claim定義有很大的差別,那麼.Net體系中有沒有針對jwt的實作呢?(注意這裡指的是.Net體系中而非基于.Net或者C#代碼的實作)答案是肯定的,因為在.Net體系(甚至可以說微軟體系)中也提供OIDC服務,它同時兼顧了jwt規範以及System.Security.Claims命名空間下對ClaimTypes定義。

  下圖為System.IdentityModel.Tokens.Jwt中定義的Jwt中的Claim名稱:

從零搭建一個IdentityServer——資源與通路控制

  同時該程式集中定義了JwtRegisteredClaimNames與ClaimTypes的映射關系,從圖中可以看出Jwt中的sub和nameid都将與ClaimTypes的NameIdentifier對應:

從零搭建一個IdentityServer——資源與通路控制

  注:System.IdentiyModel.Token.Jwt是AzureAD(微軟的身份驗證雲服務)對.net core的一個拓展類庫,具體參考: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/,而IdentityModel這個類庫是一個.net 基金會的開源項目,具體參考:https://github.com/IdentityModel/IdentityModel。

  另外需要注意的是在我們後續的内容中或者說identityServer4與Asp.net core Identity的內建中會用到以上兩個類庫,即會存在三個Claim名稱的互相映射關系。

  經過前面内容的介紹,如果要實作IdentityServer4與Asp.net Core Identity的內建那麼隻需要實作基于Asp.net core Identity的Profile Service同時完成相關Claim名稱映射即可。

  關于前者在IdentityServer4.AspnetIdentity中提供了相應的實作,它依賴Identity的UserManager和一個ClaimsFactory,具體如下圖所示:

從零搭建一個IdentityServer——資源與通路控制

  其中該類型通過UserManager來擷取使用者資訊:

從零搭建一個IdentityServer——資源與通路控制

  而ClamsFactory它更是于UserManager息息相關,它通過UserManager來擷取使用者、Email、電話号碼等相關資訊:

從零搭建一個IdentityServer——資源與通路控制

  更多細節可直接檢視相關源碼:

  https://github.com/IdentityServer/IdentityServer4/blob/main/src/AspNetIdentity/src/ProfileService.cs

  https://github.com/IdentityServer/IdentityServer4/blob/main/src/AspNetIdentity/src/UserClaimsFactory.cs

  最後就是Claim的映射問題,在介紹它們的Claim映射之前,我們先通過一個圖來介紹一些相關關系:

從零搭建一個IdentityServer——資源與通路控制

  上圖中包含兩個主體:基于is4的授權應用和基于OIDC的用戶端應用(紅色框),分别用于釋出Token和驗證Token并擷取使用者資訊,它們都是Asp.net Core應用程式,分别通過依賴IdentityServer4和Microsoft.AspNetCore.Authentication.OpenIdConnect來實作相應功能。

  三個Claim定義(文章前面提到過):IdentityServer4的Token和使用者資訊都是基于JwtClaimTypes來生成的,實際上應該說IdentityServer4實作了Jwt、Oauth2.0、OIDC協定。

  而Asp.net core應用程式預設使用System.Security.Claims.ClaimTypes。它的定義沒有jwt那麼簡潔,比如Jwt中的sub一般代表使用者的Id,而ClaimTypes中使用NameIdentifier表示(一串很長的uri)。

  JwtRegisteredClaimNames是微軟身份雲服務的一個實作,它與JwtClaimTypes存在一些差異,同時它為了能夠與Asp.net Core應用內建,自己包含了一個與ClaimTypes的映射關系。

  最後還有兩個最重要的産物ID Token、UserInfoEndpoint傳回的使用者資訊以及.Net Core應用中的User資訊,這也是IdentityServer4與Asp.net Core Identity的內建的關鍵,換句話說隻要将ID Token及UserInfo“翻譯”為.Net Core應用的User執行個體就認為它們內建成功了(使用者資訊的擷取或者說ID Token及UserInfo生成時使用者資料的來源不一定是asp.net identity,是以它不是內建的關鍵)。

  下面來做一個簡單的實驗,首先通過授權碼流程對應用程式進行身份驗證,并獲得相應ID Token以及UserInfo(詳見:https://www.cnblogs.com/selimsong/p/14355150.html#oidc_code_flow,另外需要注意的是本實驗将用戶端程式oidc身份驗證的GetClaimsFromUserInfoEndpoint配置設為true,這樣才能拿到使用者的name資訊):

User資訊如下圖所示:

從零搭建一個IdentityServer——資源與通路控制

  從圖中可以看到Claims清單中包含了使用者名資訊(name),但是User中的Name屬性卻為null,實際上從圖中就能看出原因,是因為Claims清單中的使用者名屬性Claim名稱為“name”,而User所需要的是“System.Security.Claims.ClaimTypes.Name”,是以無法正确比對。這裡需要注意的就是ID Token中包含的sub資訊卻能正确的被“System.Security.Claims.ClaimTypes.NameIdentifier”比對。

  ID Token中的sub資訊:

從零搭建一個IdentityServer——資源與通路控制

  首先需要明确的一點是IdentityServer4生成ID Token或者UserInforEndPoint擷取的使用者資訊均基于jwt規範(https://www.iana.org/assignments/jwt/jwt.xhtml),而.Net Core中oidc身份驗證元件是基于System.IdentityModel.Tokens.Jwt.ClaimTypeMapping來進行比對的,從下圖中可以看到sub比對了NameIdentifier,是以使用者Id能夠被轉換,但是該映射類型中沒有定義使用者名(name)的映射資訊,是以導緻使用者名無法被正确比對:

從零搭建一個IdentityServer——資源與通路控制

  為了能夠正确映射,我們隻需要再用戶端程式将oidc Token驗證選項中NameClaimType屬性變更為JwtClaimTypes.Name(name)即可:

從零搭建一個IdentityServer——資源與通路控制

  再次擷取的使用者資訊,資料已經成功比對上了:

從零搭建一個IdentityServer——資源與通路控制

  上面内容通過Identity Resources使用者身份資訊來引出了Claim的概念,通過Claim來對使用者資訊屬性進行映射和管理,對于API Resources來說也是一樣的,仍然是通過Claim來對API資源進行聲明,下面就來示範一下如何通過Claim定義API Resource以及如何使用這些被定義的Claim保護真實的API資源。

  首先我們假設有一系列使用者管理功能API資源,包含了使用者資訊檢視和修改。那麼根據API資源的定義,我們将該使用者管理功能定義為一個API資源,同時将使用者資訊檢視和修改以Claim的方式展現:

從零搭建一個IdentityServer——資源與通路控制

  資源中Scope的定義:

從零搭建一個IdentityServer——資源與通路控制

   

從零搭建一個IdentityServer——資源與通路控制

  然後建立一個API項目,在API項目中定義使用者管理的兩個API:

從零搭建一個IdentityServer——資源與通路控制

  然後在Startup類型的ConfigureServices方法中添加基于聲明的身份驗證政策(參考:https://docs.microsoft.com/en-us/aspnet/core/security/authorization/claims?view=aspnetcore-5.0):

從零搭建一個IdentityServer——資源與通路控制

  并把身份驗證政策添加到API的授權特性上:

從零搭建一個IdentityServer——資源與通路控制

  最後我們将相應的Scope配置到Client資訊上,并且Client在發起授權請求時添加相應的Claim資訊:

從零搭建一個IdentityServer——資源與通路控制

  Client的OIDC身份驗證配置添加需要請求的scope,這裡需要注意的是代碼中僅添加了user_read這個scope,雖然目前client資訊包含user_read和user_edit兩個scope,但是如果不進行主動請求,那麼最終獲得的結果中不會包含user_edit聲明資訊:

從零搭建一個IdentityServer——資源與通路控制

  最後在client中添加測試代碼:

從零搭建一個IdentityServer——資源與通路控制

  嘗試運作通過client來調用被保護的API,獲得以下結果:

從零搭建一個IdentityServer——資源與通路控制

  為什麼修改使用者資訊授權被拒絕呢?對access token進行解析,可以看到token中的scope資訊僅包含user_read,沒有包含user_edit這是因為在授權請求中沒有請求user_edit的原因:

從零搭建一個IdentityServer——資源與通路控制

  同意(Consent),是最終使用者授予用戶端程式通路資源權限的應允。舉個簡單的例子來說手機号碼是最終使用者的隐私資訊,一般應用程式沒有權限直接擷取,如果需要擷取那麼需要征得使用者同意,使用者同意這個過程就是Consent。

  本文中上面的内容都是由Client本身來擷取相關資源通路權限(包括使用者資源和API資源),并沒有使用者的參與,或者說使用者的允許,Consent就是引入使用者來對Client能夠擷取的權限進行授權的功能。

  IdentityServer4的Consent是在進行授權請求之前向使用者征求允許的權限,下面就基于IdentityServer4實作一個簡單的Consent功能,實作Consent功能主要有以下幾個步驟:(注:identityServer4模闆中有預設的基于MVC的Consent實作,以下内容可以看作一個簡版的Razor Page的實作,主要是僅給出了關鍵代碼,并沒有處理代碼中可能出現的異常,僅作為示範使用)

  1. 修改Client資訊讓相應Client支援Consent。

  2. 為IdentityServer應用添加Consent頁面,頁面主要功能是将目前Client支援的資源列出給使用者選擇并将選擇結果傳遞給後續的授權請求。

  3. 對IdentityServer4進行配置,将Consent連接配接指向我們添加的頁面。

  1. 通過修改ClientRequireConsent設為true:

從零搭建一個IdentityServer——資源與通路控制

  2. 添加Consent頁面:

從零搭建一個IdentityServer——資源與通路控制

  2.1 擷取目前授權請求上下文,通過上下文擷取目前請求Client所擁有的資源并展示:

從零搭建一個IdentityServer——資源與通路控制

  這段代碼主要目的是在授權請求過程中(由于設定了需要授權Require Consent)跳轉到同意(Consent)頁面,并展現出目前Client所有可選的Scope(包括IdentityScopes和ApiScopes)供使用者進行選擇并同意目前Client通路。

  2.2 添加頁面用于展示并選擇送出使用者同意的權限或拒絕授權:

  首先定義一個用于存放使用者送出内容的模型:

從零搭建一個IdentityServer——資源與通路控制

  根據模型編寫頁面展示/送出代碼(APIScopes部分展示代碼與IdentityScopes部分類似):

從零搭建一個IdentityServer——資源與通路控制

  處理送出内容,如果點選no按鈕直接拒絕授權,如果點選yes則完成授權,并跳轉完成後續授權請求工作:

從零搭建一個IdentityServer——資源與通路控制

  3.配置IdentityServer4的Consent頁面路徑:

從零搭建一個IdentityServer——資源與通路控制

  4. 運作程式進行測試(使用上一章的UserManage功能進行測試):

  首先通路受保護資源UserManage時先跳轉到登入頁面,完成登入後就可以看到剛剛建立的Consent頁面:

從零搭建一個IdentityServer——資源與通路控制

  點選同意按鈕後得到以下結果,注意修改使用者的狀态碼是200:

從零搭建一個IdentityServer——資源與通路控制

  如果取消修改使用者資訊權限:

從零搭建一個IdentityServer——資源與通路控制

  那麼就會看到修改使用者資訊被403拒絕的資訊:

從零搭建一個IdentityServer——資源與通路控制

  5. 添加一個電話号碼的身份資源,并賦予到相應的Client後:

  首先定義資源(phone資源定義參考:https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims):

從零搭建一個IdentityServer——資源與通路控制

  資源下包含“phone_number”Claim:

從零搭建一個IdentityServer——資源與通路控制

  将phone這個資源作為Client允許的Scope:

從零搭建一個IdentityServer——資源與通路控制

  為使用者資料添加電話号碼資訊:

從零搭建一個IdentityServer——資源與通路控制

  運作程式後可以看到Consent頁面已經有“電話号碼”這個使用者資訊資源授權:

從零搭建一個IdentityServer——資源與通路控制

  但是點選同意後Client中的UserClaims中并沒有電話号碼相關的資訊:

從零搭建一個IdentityServer——資源與通路控制

  是因為資料沒生效嗎?我們知道這裡的使用者資訊來自于UserInfoEndpoint,它是通過攜帶access token來完成使用者資訊請求的,那麼首先我們來看看生成的access token包含哪些資訊?

從零搭建一個IdentityServer——資源與通路控制

  已經看見它有權通路phone這個scope資訊了,但是為什麼沒有相應資料呢?我們通過這個access token嘗試通路一次UserEndPoint看看:

從零搭建一個IdentityServer——資源與通路控制

  能夠看到已經有phone_number這個資料了,是以最終的問題出在UserInfoEndpoint資料與Asp.net Core User對象資料映射的時候,僅需要添加以下配置即可将phone_number映射到User中:

從零搭建一個IdentityServer——資源與通路控制

  重新登入後得到以下結果:

從零搭建一個IdentityServer——資源與通路控制

  注意,由于asp.net core應用程式有一些預設的claim映射和過濾,會導緻與真實傳回的Token結果不一緻,可以通過下面代碼禁用這些映射關系:

從零搭建一個IdentityServer——資源與通路控制

  禁用這些關系後再次登入,可以看到claim資訊與之前有很大的差異,現在的claim基本與jwt協定的claim定義一緻了:

從零搭建一個IdentityServer——資源與通路控制

  本文介紹了IdentityServer或者說OIDC協定中對資源的定義與通路控制,對比了基于jwt的Claim定義與.Net體系中Claim定義的差別,了解到OIDC協定或者IdentityServer4與Asp.net core應用內建時關鍵在于Claim的映射。

  同時文章最後通過IdentityServer4的Consent功能實作了使用者對Client所需權限的授權。Consent功能将預設的授權變為使用者主動授權,這樣做更利于資源的控制和使用者隐私的保護。

參考:

https://identityserver4.readthedocs.io/en/release/

https://stackoverflow.com/questions/57860625/difference-between-claimactions-remove-and-claimactions-deleteclaim

https://damienbod.com/2019/11/01/user-claims-in-asp-net-core-using-openid-connect-authentication/

本文位址:https://www.cnblogs.com/selimsong/p/15032887.html

從零搭建一個IdentityServer——目錄(更新中...)

作者:7m魚

出處:http://www.cnblogs.com/selimsong/

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。

從零搭建一個IdentityServer——資源與通路控制

繼續閱讀