從零搭建一個IdentityServer——聊聊Asp.net core中的身份驗證與受權

  OpenIDConnect是一個身份驗證服務,而Oauth2.0是一個受權框架,在前面幾篇文章裏經過IdentityServer4實現了基於Oauth2.0的客戶端證書(Client_Credentials)、用戶名密碼(Password)的受權流程,同時也實現OpenIDConnect的受權碼(Authorization Code)、隱式流程(Implicit)的身份驗證。
  ???啥?一下子是受權一下子是身份驗證,身份驗證與受權傻傻分不清楚??本文就來聊一聊Asp.net core中的身份驗證與受權。
  本文主要內容有:

身份驗證與受權

  之前寫過一篇asp.net identity的文章( https://www.cnblogs.com/selimsong/p/7828326.html)已經提到過身份驗證與受權的概念,簡單來講身份驗證就是「是誰」的問題,而受權就是「能不能」的問題,通常來講首先須要知道「是誰」,而後再判斷「能不能」。
  這裏舉個生活中常見的小栗子,鎖是門用來保護門內財產的工具,而隨着科技發展示在有了指紋鎖,指紋鎖的特徵是它既能夠經過指紋來開鎖,也能夠經過鑰匙開鎖,對於指紋開鎖時首先須要錄入指紋並指定一個指紋身份,好比保姆阿姨,首先須要的就是給她錄入指紋,而後容許該指紋在上午6點至晚上10點能夠開門,那麼最終保姆阿姨在開門時,受權識別指紋, 經過指紋匹配到或者說知道是保姆,這裏就是身份驗證,若是陌生人進行指紋匹配那麼將匹配不到任何身份,可是可否開門還得根據設定的規則,那就是開門時間是否在規定的時間範圍內, 知足條件才能開門,這就是受權
  固然在開門這個問題上還有一個Bug,那就是鑰匙,只要擁有鑰匙,無論是誰都能開門, 得到鑰匙就是得到受權
  在軟件系統中一般使用的用戶名密碼登陸實際上就是身份驗證功能,用戶登陸後系統就記住這一狀態,後續訪問系統時系統就知道「是誰」在訪問系統,而後由於已經知道是誰,那麼就能夠根據具體訪問條件來判斷用戶「能不能」訪問資源,這就是受權。

Asp.net core中的身份驗證與受權

  首先須要再次明確一下Asp.net core是一個Web框架,它自己就具備一些特性,這其中就包括了身份驗證和受權。
  在Asp.net core中的身份驗證和受權是經過中間件完成的,而把一箇中間件添加到asp.net core的應用程序中通常只須要兩個步驟,第一是對相關中間件所需參數及服務進行配置,第二就是將相應的中間件添加到請求管道中便可。
  下圖爲基於OpenIDConnect客戶端程序的身份驗證配置:
  
  下圖爲基於OpenIDConnect客戶端程序的身份驗證及受權中間件配置:
  
  以上代碼並無額外的配置受權策略,可是能夠經過Authorize特性來提供最基礎的受權(受權經過身份驗證的用戶)。另外須要注意的是Authorize特性是須要搭配Authorization中間件來使用的,以下圖所示:
  
  另外基於Identity組件的身份驗證代碼中沒有出現AddAuthentication及AddCookie方法,而是經過AddDefaultIdentity就能夠完成身份驗證,是由於AddDefaultIdentity方法中包含了相關方法調用:
  
  AddDefaultIndeity方法代碼:
  完成配置後就能夠在應用程序中使用身份驗證及受權功能了。
  關於asp.net core官方提供的身份驗證方式,咱們能夠直接看看GitHub上的代碼:
  
  從圖中能夠看到有基於Cookie、Jwt Bearer、Oauth、OpenIdConnect也有基於Facebook、Google、MicrosoftAccount、Twitter的,若是非官方的話應該還能找到基於微信、支付寶等帳號的登陸開源庫。
  總的來講asp.net core的身份驗證能夠支持現有的大部分經常使用方式或協議,同時也支持第三方的帳戶登陸。

Asp.net core身份驗證及受權的基本原理

Scheme與身份驗證處理器

  Scheme和處理器能夠簡單的理解爲一個鍵值對,處理器是用於實際處理身份驗證邏輯的代碼,Scheme就是這個處理器的標識,經過Scheme能夠直接獲取到相應的處理器,而後經過處理器來完成身份驗證。
  Scheme是一個重要的概念,由於在asp.net core中它能夠添加多個身份驗證處理器,在Asp.net版本中,或者準確來說Owin中咱們就提到過一個多重身份驗證的概念( ASP.NET沒有魔法——ASP.NET Identity 的「多重」身份驗證)實際上也就是在一個應用裏面添加了多個身份驗證處理器,換句話說就是一個應用程序支持多種身份驗證(登陸)方式。asp.net core中管理多個身份驗證處理器的核心就是基於Scheme,還記得本文上面oidc驗證添加的服務配置代碼嗎。
  
  在這段代碼中設置了身份驗證的默認Scheme以及默認ChallengeScheme,關於Scheme的做用請往下看。
  注:asp.net 與asp.net core中的身份驗證機制有共同點也有區別,整體來講asp.net core基於scheme的身份驗證管理機制邏輯上和性能上會更好(畢竟是最新的產物)。
  關於身份驗證處理器,它實際上就是一個實現IAuthenticationHandler接口的類型,它提供了身份驗證所需的具體實現邏輯:
  

三個方法Authenticate、Challenge、Forbid

  這三個方法是asp.net core身份驗證/受權中的基礎,它們分別表明身份驗證、質疑和禁止,每個身份驗證處理器都須要實現這三個方法,下面簡單介紹一下這三個方法:
   Authenticate:
  • 身份驗證調用和核心邏輯,換句話就是證實「是誰」的方法。
  • 擬人化來講就是檢查身份證同時與持有人是否匹配的過程。
  • 在程序中就是檢查cookie、jwt token、id token等是否有效,以及信息載體中標記的用戶「是誰」
   Challenge:
  • 可翻譯爲「懷疑/質疑」,實際上就是身份驗證沒有成功後調用的方法。
  • 擬人化來講就是「我」不知道你「是誰」,但「我」須要知道,因此「我」會問「你是誰?把你的身份證給我看一下?」
  • 在程序中通常的過程就是重定向到登陸頁面,經過登陸方式告訴系統「是誰」。對於Api一類沒有UI的程序時,就返回401狀態碼告知未經過身份驗證。
   Forbid:
  • 這個方法用於受權,受權失敗時調用該方法。
  • 這個方法相對簡單,當程序存在UI時,經過UI告知用戶無權限禁止訪問便可,對於Api一類沒有UI的程序時,經過返回403狀態碼告知無權限。

兩個中間件AuthenticationMiddleware、AuthorizationMiddleware

  身份驗證中間件(AuthenticationMiddleware),只作三件事:
  1. 處理身份驗證請求,如oidc的由身份驗證服務器完成id_token生成跳轉的/signi-oidc。
  2. 處理 默認scheme的身份驗證流程。
  3. 若是身份驗證經過後將驗證結果的 主體信息(Principal)放到HttpContext中
  
  受權中間件(AuthorizationMiddleware)主要是經過一系列終結點受權信息獲取、執行後根據受權執行結果來決定是challenge、forbidden仍是擁有權限可進入資源訪問(參考:   https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs   https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authorization/Policy/src/AuthorizationMiddlewareResultHandler.cs):
  
  注:若是所訪問的資源沒有受權相關的限制,那麼請求將跳過受權步驟直接往下訪問。

三個對象HttpContext、ClaimsIdentity、AuthenticationProperties

  首先咱們來看看ClaimsIdentity,它其實是一組Claim的集合,每一個Claim表明用戶身份的一個屬性的鍵值對,一組Claim能夠表示某一方面的用戶信息特性,除此以外它還包含是否經過驗證(IsAuthenticated)以及驗證方式(AuthenticationType)等信息。
  下圖爲經過oidc身份驗證的ClaimsIdentity信息,HttpConext對象中包含的User是ClaimsPrincipal(聲明的主體),一個主體裏面包含多個ClaimsIdentity信息:
  
  這裏能夠這麼理解這些對象:
  1. 咱們每一個人都有身份證、護照、戶口冊、駕照等能夠證實咱們身份的東西,這至關於一個ClaimsPrincipal能夠擁有多個ClaimsIdentity。
  2. 身份證上面有姓名、身份證號等屬性,至關於一個ClaimsIdentity包含多個Claim。
  3. 關於Claim它表明一個用戶信息屬性,而且一些屬性名稱是有相關定義的,具體參考: https://docs.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes?view=net-5.0
  4. 每一個身份證實它的識別方法不同,好比身份證能夠經過身份證識別器識別、戶口冊能夠在公安局識別,這個至關於每一個ClaimsIdentity中的AuthenticationType。
 
   AuthenticationProperties:它是一個用來存儲身份驗證會話數據的字典,oidc流程中IdentityServer返回的Id_token及access_token等信息就是存儲到AuthenticationProperties中。
 
   HttpContext:Http上下文對象,是整個請求的核心,包含了Http請求及響應的全部內容,可是在身份驗證/受權方面,它有另外一個角色—— 身份驗證服務代理,經過HttpContext咱們能夠調用身份驗證服務的相關方法,包括身份驗證和受權中間件的Challenge等方法調用都是經過HttpContext完成的。
  下圖爲HttpContext在Authentication命名空間下的拓展方法定義:
  
  下圖爲IAuthenticationService的方法定義,HttpContext經過容器獲取IAuthenticationService的實例進行調用,而IAuthenticationService最終實際上調用的是指定或默認身份驗證處理器的相關方法:
  

Signin與身份信息載體

  前面文章詳細講解了身份驗證的相關細節,但惟獨沒說的就是登陸。登陸究竟是作了什麼事情?在瞭解登陸以前咱們先來了解一個概念「身份信息載體」,其實也就字面意思,承載身份信息的物體,在現實生活中咱們的身份信息載體是「身份證」等等實際物品,而在信息系統中信息載體就是一段數據,這段數據爲了能讓相關程序或者廣大程序所理解,它應該按照具體的協議來建立,信息系統中經常使用的身份信息載體有Cookie以及Jwt(Json web token)。
   Cookie:
  咱們都知道http是一個無狀態協議,可是大部分時候咱們須要它「有」狀態,Cookie做爲一項瀏覽器數據存儲技術,它常常用於存儲一些狀態信息,用於下一次發起請求的時候服務器可以瞭解當前請求的狀態。因此Cookie很是適合做爲身份信息載體,固然asp.net core的基於Cookie身份驗證是這樣作的,將用戶信息(ClaimsIdentity)加密後存儲到Cookie中,下次從Cookie中獲取數據,解密後得到用戶信息並完成身份驗證。
   Jwt:
  Jwt是一種基於Json的安全信息傳輸標準,Jwt由於帶有數字簽名的,能夠保證數據完整性,就想咱們的身份證同樣不能僞造,因此也很適合做爲身份信息載體。
 
  Cookie和Jwt各有特色,可適用於不一樣的應用場景,如Cookie它自己有域特性,如今的單頁應用程序它會存在跨域問題,而Jwt雖然能保證數據完整,可是它自己不是加密的(可是傳輸過程能夠加密,而且生產通常必須加密,如https),因此Jwt中的身份信息很容易泄漏,因此它比較適合更封閉的客戶端,如服務端與服務端通信、手機App等。
 
  如今咱們再回來聊登陸, 登陸實際上就是將身份信息寫到身份信息載體的過程。基於Cookie的就寫Cookie,基於Jwt的就頒發Jwt,可是須要注意的是通常jwt由第三方身份驗證服務器頒發,因此應用程序自己是不須要關注的,因此這裏主要講講基於Cookie的登陸。
  下面咱們作一個基於Cookie登陸的小實驗,首先作一個簡單的基於Identity的登陸功能:
  
  設置斷點後,直接訪問登陸頁面進行登陸,在登陸信息提交後咱們能夠看到User信息是空的:
  
  登陸以後仍然沒有用戶信息:
  
  可是在ResponseHeader的HeaderSetCookie信息中咱們找到了以下信息:
  看到它即將寫入cookie中帶有它建立的身份信息載體。這個就是登陸生成身份信息載體的過程,至於登錄後便可訪問保護內容,是由於登陸完成後作了跳轉,跳轉後將攜帶身份信息發起請求後既能夠完成身份驗證,從而能夠訪問受保護內容。
  注:Identity提供的登陸功能最終也是經過HttpContext的拓展方法經過IAuthenticationService來完成的,具體可參考相關源碼,這裏不在贅述。

自主登陸與外部登陸

   自主登陸指的是應用程序自己提供了用戶身份覈對(用戶名+密碼登陸),而後擁有用戶信息自主權(應用程序保存了與用戶相關的信息),最後根據用戶信息來生成用戶信息載體的登陸方式。如Asp.net core Identity提供的就是一種自主登陸方式。
   外部登陸指的是由第三方程序來對用戶身份覈對,並提供相關用戶信息交由程序自己來生成用戶信息載體的,或者直接由第三方程序生成用戶信息載體的方式。
  如本系列文章介紹的oidc的身份驗證就是由IdentityServer提供用戶身份覈對並提供用戶信息(UserInfo EndPoint),而後交由客戶端程序來生成身份信息載體Cookie。
  而若是經過IdentityServer直接經過Oauth2.0流程得到Access Token的方式就至關於由第三方程序生成用戶信息載體,客戶端直接驗證用戶信息載體便可完成後續的身份驗證。

Asp.net core身份驗證及受權流程

  前面內容詳細介紹了Asp.net core身份驗證相關的一些基礎原理,下面就經過一個流程圖來介紹一下完整的身份驗證和受權流程:
  
  從圖中咱們能夠找到3個主體分別是:瀏覽器、Asp.net core應用程序以及第三方驗證服務。
  整個流程的開始多是經過訪問受保護資源、自主登陸系統或者外部登陸系統開始,可是登陸的目的在於訪問受保護資源,下面就簡單對訪問受保護資源流程進行梳理:
  1. 瀏覽器發起受保護資源訪問請求(沒有Cookie).
  2. 服務器對請求進行身份驗證,由於沒有Cookie返回一個失敗結果。
  3. 由於驗證結果爲失敗,因此沒有ClamsIdentity信息,賦值到HttpContext.User也爲空。
  4. 進行受權判斷,由於沒有通過身份驗證,因此調用質疑操做(Challenge),由默認的ChallengeScheme決定是自主登陸仍是外部登陸。
  5. 若是是自主登陸,那麼跳轉到應用登陸頁面完成登陸,並根據用戶信息生成ClaimsIdentity。
  5. 若是是外部登陸,那麼跳轉到第三方登陸頁面完成登陸,並回到自主應用的回調地址對第三方返回的code、id_token及access_token進行處理,並獲取用戶信息,根據獲取的用戶信息生成ClaimsIdentity。
  6. 系統將ClaimsIdentity信息生成身份信息載體(Cookie)並重定向回以前訪問的資源。
  7. 重定向後攜帶身份信息載體訪問受保護資源,若是用戶有權限,那麼可訪問資源,若是沒有權限返回403禁止訪問。
 
  小提示:爲何asp.net core identity生成的UI代碼中,外部登陸執行的核心代碼爲ChallegeResult + (provider 和returnUrl)?
   

Asp.net core中的受權

  前面詳細介紹了Asp.net core中的身份驗證,受權僅僅是其中的一環來幫助完成身份驗證。那麼Asp.net core中提供了哪些受權機制或者說要如何進行受權呢?
  Asp.net core及Identity組件提供了簡單的(只要經過身份驗證)、基於角色的、基於聲明(Claim)的、基於策略的受權機制,具體使用方式參考文檔: https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction?view=aspnetcore-5.0
  另外還給了一個如何實現數據增刪改權限控制的例子:
  上面這個例子告訴咱們受權機制 不只僅侷限於受權特性和中間件,咱們能夠把受權機制融入到咱們的業務邏輯中。

小結

  本篇文章從Asp.Net core介紹了身份驗證和受權的基本概念和原理,經過流程圖的方式展示了Asp.net core身份驗證和受權的流程,最後簡單介紹了受權的相關機制。
  如今咱們回到文章開頭問的問題爲何IdentityServer4提供的功能中一下子是身份驗證,一下子是受權??
  這個問題須要根據主體來看,首先咱們看Oauth2.0,它的最終結果是一個Jwt的Bearer Token,這至關於給了你一把鑰匙,使用這個鑰匙你能夠打開指定的門,因此它是一個受權。
  而後來看看OIDC的受權碼流程,它除了Access Token外實際上關鍵的是Id_token,證明了用戶的身份,這至關於告訴你,用戶是保姆阿姨,解決了「是誰」的問題,因此是身份驗證。知道了是誰,至於開不開門,那是你(客戶端程序)的受權問題。
  最後來看看Asp.net core應用程序,在Asp.net core應用程序中不存在獨立的受權,換句話就是無法單獨使用受權功能,須要身份驗證和受權功能聯合使用,好比Oauth給了一把鑰匙,可是Asp.net core仍要對鑰匙進行驗證,看清楚鑰匙上貼了張三的名字,但頗有可能這把鑰匙是李四拿着。
 
參考:
以及文章中涉及的相關源代碼
相關文章
相關標籤/搜索