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

7m魚發表於2021-02-03
  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仍要對鑰匙進行驗證,看清楚鑰匙上貼了張三的名字,但很有可能這把鑰匙是李四拿著。
 
參考:
以及文章中涉及的相關原始碼

相關文章