從零搭建一個IdentityServer——單頁應用身份驗證

7m魚發表於2021-03-04
  上一篇文章我們介紹了Asp.net core中身份驗證的相關內容,並通過下圖描述了身份驗證及授權的流程:
  
  注:改流程圖進行過修改,第三方使用者名稱密碼登陸後並不是直接獲得code/id_token/access_token,而是登入後可以訪問identityServer中受保護的資源(Authorize Endpoint),通過發起身份驗證請求來實現授權碼流程、隱式流程及混合流程來完成token的獲取,它與直接通過使用者名稱密碼來獲取token的Oauth2.0 Password GrantType方式是不一樣的。
  在asp.net core應用程式中,通過授權碼流程可以使用第三方(IdentityServer)的使用者名稱密碼,經過一系列的token、userinfo獲取,最後生成身份資訊載體(Cookie),asp.net core應用程式使用cookie就能完成身份驗證工作。這個過程對於使用者來說,它與一般的asp.net core應用程式(特指基於asp.net core identity的應用程式)是沒有任何區別的,都是通過使用者名稱密碼登入,然後就可以進入系統。對於應用程式來說它仍然是基於cookie來完成身份驗證,只不過生成cookie所需的資料是第三方提供的而已。
  但是單頁應用由於其特殊性,其UI渲染工作及業務邏輯的處理都是由瀏覽器完成,伺服器不具備相關功能(僅需靜態檔案傳輸即可),其次單頁應用會存在跨域問題,所以cookie就不適合作為單頁應用的身份資訊載體,本文就介紹如何使用jwt來完成單頁應用的身份驗證。
  主要內容有:
  • 建立一個簡單的單頁應用專案
  • 使用單頁應用完成受保護資源訪問
  • oidc Client簡介
  • oidc-client.js的各種用法
    • 彈出式登入/登出
    • 靜默登入與靜默重新整理
    • 會話監聽
  • 小結

建立一個簡單的單頁應用專案

  注:該單頁應用完全參考官方文件,本小節僅對要點進行介紹,詳情參見文件:https://identityserver4.readthedocs.io/en/latest/quickstarts/4_javascript_client.html
  1、新建一個asp.net core的web應用程式:
  2、新增oidc的js元件:
  可以通過npm進行安裝或者在Github上直接下載,以下為npm安裝方法:
  npm i oidc-client
  
  完成之後在相關目錄下可找到oidc-client相關檔案:
   
  將其複製到適合的位置即可。
  3、通過資料庫新增一個Client資訊(如果是基於記憶體的,那麼需要新增一個client例項,詳見文件:https://identityserver4.readthedocs.io/en/latest/quickstarts/4_javascript_client.html#add-a-client-registration-to-identityserver-for-the-javascript-client),用於單頁應用的授權配置:
  新增Client時需要注意幾個關鍵資訊:
  • 授權型別支援授權碼型別;
  • 不需要客戶端密碼;
  • 允許跨域,允許客戶端跨域訪問IdentityServer;
  參考如下,記憶體例項:
  
  資料庫資料:
  
  4、新增基於oidc-client的登入、API訪問以及登出業務邏輯程式碼App.js:
  UserManager物件初始化:
  
  使用UserManager例項進行登入跳轉:
  
  使用UserManager例項獲取使用者資訊,然後通過使用者資訊中的access token訪問受保護資源:
  
  使用UserManager例項進行登出跳轉:
   
  5、新增功能頁面Index.html,包含登入、API訪問及登出功能:
  
  6、新增用於處理授權碼的重定向頁面:
  
  到此單頁應用程式已經建立完畢,後面就使用該程式要介紹它是如何完成身份驗證,並訪問受保護資源的。

使用單頁應用完成受保護資源訪問

  我們使用一個簡單的asp.net core web api專案(本系列文章用過的)來進行演示,它對於普通API專案來說要點在於:
  1、新增基於JwtBearer的身份驗證處理器:
  
  2、新增跨域處理,新增跨域策略配置:
  
  3、在asp.net core應用請求管道中應用跨域配置:
  
  4、受保護內容通過authorize特性進行標記:
  
  一切準備就緒後執行三個應用程式,單頁應用執行並開啟index.html頁面效果如下,一共有三個功能,登入、呼叫API以及登出:
  
  登入:它實際上是呼叫oidc Client的signinRedirect方法,語義上來說它是通過重定向的方式進行登入,而它實際執行的效果如下:
  跳轉到了IdentityServer的登入頁面,然後我們再看看它本質上是做了什麼?
  它實際上是發起了一個授權碼流程的身份驗證請求(請求過程可參考:https://www.cnblogs.com/selimsong/p/14355150.html#oidc_code_flow),發起請求後,由於當前使用者沒有在IdentityServer上登入或者說未通過IdentityServer的身份驗證,所以由跳轉到登入頁面。
  當我們通過使用者名稱密碼登入之後,IdentityServer將繼續完成授權碼流程,後續流程是生成相應的授權碼並返回到客戶端配置的重定向uri(本例中是https://localhost:5003/html/callback.html),
為了能夠看清楚整個請求過程,本例在callback.html頁面加入了除錯斷點:
  
  斷點位於signinRedirectCallback方法之後,也就是完成回撥處理之後(這個時候已經完成token等資訊的獲取),跳轉到index.html頁面之前。
  以下是輸入使用者名稱密碼提交後命中斷點時的相關請求資訊:
  由上圖可以看到,當輸入使用者名稱密碼提交後(第一個請求),由於通過了身份驗證,那麼繼續完成授權碼流程(第二個請求),授權碼流程完成後攜帶授權碼重定向到Client配置的重定向地址(第三個請求).
  第三個請求就到了我們的callback.html頁面,頁面的載入首先請求了oidc-client.js檔案,然後由UserManager的例項化以及signinRedirectCallback方法,來完成了後續請求,後續請求包含openid的配置資訊請求、獲取Token請求、獲取使用者資訊(userinfo)請求以及檢查會話請求。
  以上一系列的請求結果就是在瀏覽器的會話儲存中,我們可以找到相關的資料資訊:
  
  斷點通過後就來到了index.html頁面,並列印出登入使用者資訊:
  
  點選Call API按鈕後,程式將從儲存資訊中獲取到access_token,攜帶access_token完成請求:
  點選登出按鈕後,程式將刪除使用者資訊並跳轉到IdentityServer的登出頁面:
  注:需要配置identityserver4的登出url:
  

oidc-client.js簡介

  前面的內容是基於oidc-client.js,即JavaScript版本的oidc客戶端類庫來實現的單頁應用的,那麼oidc-client.js到底為我們提供了什麼功能呢?
oidc-client.js是一個支援OIDC和Oauth2.0協議的JavaScript類庫,除此之外它還提供使用者會話和Token的管理功能。類庫中的核心型別是UserManager,它提供了使用者登入、登出、使用者資訊管理等高層次的API,上面的例子中就是使用UserManager來完成的登入、使用者資訊(Access Token)獲取以及登出的。
oidc-client.js或者直接說UserManager使用上需要注意以下幾個方面:
  • 配置:配置的目的和asp.net core基於oidc身份驗證的配置類似,主要是指明identityServer的地址、用於授權的Client資訊、授權所使用的流程(授權碼還是隱式流程)、授權完成後的跳轉地址以及請求的Scope資訊等(更多配置引數可檢視文件:https://github.com/IdentityModel/oidc-client-js/wiki),如下圖所示:

  

  但這裡要注意的是由於以上程式碼對使用者是可見的,所以Client的密碼就省略了。
  • 方法:提供了使用者管理、登入、登出、以及相關回撥方法,除此之外還有會話狀態查詢和開啟/關閉靜默重新整理(token)的方法。登入/登出分為三種型別:跳轉、靜默和彈出,具體如何使用後續介紹。

 

  • 屬性:可以返回UserManager的配置、事件以及後設資料服務。
  • 事件:UserManager包含了8個事件,如使用者登入/登出、access token過期等:

  

oidc-client.js的各種用法

彈出式登入/登出

  彈出式登入/登出就是字面的意思,通過彈出視窗開啟IdentityServer的登入/登出頁面完成相應功能。
  下圖為彈出式登入(僅需呼叫UserManager的signinPopup方法即可):
  
  注:回撥頁面需要使用signinPopupCallback:
  
  下圖為彈出式登出:
  

靜默登入與靜默重新整理

  靜默登入和靜默重新整理指的就是signinSilent和startSilentRenew兩個方法,而且需要注意的是startSilentRenew的原理實際上是關注了accessTokenExpiring事件,當token即將過期時呼叫signinSilent進行靜默登入。
  靜默登入方式又有兩種其一是基於會話的,其二是基於重新整理token,其中重新整理token的優先順序較高,換句話就是重新整理token存在的時候,它就預設使用重新整理token進行登入,重新整理token比較好理解,但是會話是什麼呢?它實際上就是通過IdentityServer的登入後所保持的狀態,文章最開始的流程圖中提到過,我們之所以可以通過授權碼流程進行授權是因為登入之後有權訪問IdentityServer受保護的授權終結點,從而可以獲取授權碼及相關Token,那這裡的原理就是瀏覽器儲存了登入狀態,所以可以再次訪問授權終結點來獲取並重新整理Token資訊。
  基於會話的靜默登入,下圖為點選靜默登入按鈕後發起的請求資訊,也就是正常的請求到授權碼之後獲取token及使用者資訊的過程:
  需要注意的是回撥頁面需要使用signinSilentCallback方法,同時不再需要頁面跳轉:
  
  基於重新整理token的靜默登入,在嘗試重新整理token登入之前首先需要獲得重新整理token,oidc中重新整理token的獲取是需要client支援offline_access的scope,同時在發起獲取token時攜帶該scope:
  
  配置完成後重新登入獲取token即可在儲存中找到重新整理token資訊 :
  
  然後再次進行靜默登入(相應client需要支援refresh_token的授權方式):
  
  靜默登入發起的請求資訊:
  
  響應資訊中包含了新的token:

會話監聽

  會話監聽是預設開啟的,在正常登入狀態下,通過新的瀏覽器視窗從identityserver中登出(目的是清除identityserver儲存在瀏覽器的會話資訊):
  
  資訊清除後它會立即嘗試發起新的身份驗證請求,但是返回資訊中包含“需要登入”錯誤資訊,可以在接收到相關錯誤資訊時清除相關Token及使用者資訊,已達到單頁應用隨著IdentityServer會話結束而登出的效果:

小結

  本篇文章介紹了單頁應用使用IdentityServer進行身份驗證的過程及oidc-client.js JavaScript類庫在應用中的使用,oidc-client.js為我們適配了oidc協議,同時還提供了豐富的功能和機制,使用這個類庫可以大大減少實際工作中的開發量。
  說到單頁應用的身份驗證,它最根本的機制無非就是獲得Access Token和Refresh Token,使用Access Token作為身份資訊載體來完成身份驗證,使用Refresh Token作為更新Access Token的鑰匙,通過保證Access Token不過期來保證使用者能夠正常訪問相關資源。
  但如果使用Oauth2.0協議來實現單頁應用的登入(Password授權模式)會存在一些問題,首先是單頁應用對於授權伺服器來說是不可信的,但是Password授權由單頁應用發起,屬於授權伺服器的使用者資訊及密碼都要經過不可信的單頁應用,這會造成一些安全問題,其次使用Oauth2.0協議完成授權後,應用與授權伺服器之間實際上就沒有任何關聯了,授權伺服器不保留使用者會話,也無法實現使用者登出聯動的功能(即一方登出可以通知另一方),這些也正是IdentityServer4或者說OIDC協議所處理的內容。
  下篇文章,我們將繼續以IdentityServer4或者說OIDC的會話管理開始,深入聊一聊它們的登入與登出。
 
參考:

相關文章