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

  上一篇文章我們介紹了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的各種用法
    • 彈出式登錄/登出
    • 靜默登錄與靜默刷新
    • 會話監聽
  • 小結

創建一個簡單的單頁應用項目

  註:該單頁應用完全參考官方文檔,本小節僅對要點進行介紹,詳情參見文檔://identityserver4.readthedocs.io/en/latest/quickstarts/4_javascript_client.html
  1、新建一個asp.net core的web應用程式:
  2、添加oidc的js組件:
  oidc的js客戶端庫://github.com/IdentityModel/oidc-client-js
  可以通過npm進行安裝或者在Github上直接下載,以下為npm安裝方法:
  npm i oidc-client
  
  完成之後在相關目錄下可找到oidc-client相關文件:
   
  將其複製到適合的位置即可。
  3、通過資料庫添加一個Client資訊(如果是基於記憶體的,那麼需要添加一個client實例,詳見文檔://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的登錄頁面,然後我們再看看它本質上是做了什麼?
  它實際上是發起了一個授權碼流程的身份驗證請求(請求過程可參考://www.cnblogs.com/selimsong/p/14355150.html#oidc_code_flow),發起請求後,由於當前用戶沒有在IdentityServer上登錄或者說未通過IdentityServer的身份驗證,所以由跳轉到登錄頁面。
  當我們通過用戶名密碼登錄之後,IdentityServer將繼續完成授權碼流程,後續流程是生成相應的授權碼並返回到客戶端配置的重定向uri(本例中是//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資訊等(更多配置參數可查看文檔://github.com/IdentityModel/oidc-client-js/wiki),如下圖所示:

  

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

 

  • 屬性:可以返回UserManager的配置、事件以及元數據服務。
  • 事件:UserManager包含了8個事件,如用戶登錄/登出、access token過期等:

  

  以上內容參考文檔://github.com/IdentityModel/oidc-client-js/wiki

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的會話管理開始,深入聊一聊它們的登錄與登出。
 
參考: