在[認證授權]系列博客中,分別對OAuth2和OIDC在理論概念方面進行了解釋說明,其間雖然我有寫過一個完整的示例(https://github.com/linianhui/oidc.example),但是卻沒有在實踐方面做出過解釋。在這里新開一個系列博客,來解釋其各種不同的應用場景。因為OIDC是在OAuth2之上的協議,所以這其中也會包含OAuth2的一些內容。
OIDC協議本身有很多的開源實現,這里選取的是基於.Net的開源實現基於IdentityServer4。本系列的源代碼位於https://github.com/linianhui/oidc.example。clone下來后用管理員身份運行build.ps1來部署整個系統,其中可能會彈出UAC警告(腳本會修改host文件,記得允許管理員讀寫這個文件先)。部署完后的樣子如下:
本文中主要是關注一下SSO這部分的內容,主要是跨一級域的單點登錄和統一登出功能。其中涉及到的站點有一下4個:
- oidc-server.dev:利用oidc實現的統一認證和授權中心,SSO站點。
- oidc-client-hybrid.dev:oidc的一個客戶端,采用hybrid模式。
- oidc-client-implicit.dev:odic的另一個客戶端,采用implicit模式。
- oidc-client-js.dev:oidc的又一個客戶端,采用implicit模式,純靜態網站,只有js和html,無服務端代碼。
單點登錄
通常來講,SSO包括統一的登錄和統一的登出這兩部分。基於OIDC實現的SSO主要是利用OIDC服務作為用戶認證中心作為統一入口,使得所有的需要登錄的地方都交給OIDC服務來做。更直白點說就是把需要進行用戶認證的客戶端中的用戶認證這部分都剝離出來交給OIDC認證中心來做。具體的交互流程如下:
其中這三個客戶端是完全獨立的位於不同的域名之下,且沒有任何依賴關系,三者均依賴oidc-server.dev這個站點進行認證和授權,通信協議為HTTP,那么下面則通過它們之間的HTTP消息來解釋其具體的流程。這個過程中使用fiddler來進行監視其所有的請求。
第1步:OIDC-Client- 觸發認證請求
在瀏覽器打開oidc-client-implicit.dev這個站點,打開后如下(QQ這個先不管它,后面單獨介紹)。
點擊Oidc Login后,會觸發一個302的重定向操作。具體的HTTP請求和響應信息如下:
Request:Get后面的URL是我們點擊Oidc Login的Url,這個URL包含一個參數,代表登錄成功后所要回到的頁面是哪里。
Response:服務器返回了一個302重定向。
- Location的Url指向了oidc-server.dev這個站點,其中還攜帶了一大堆參數(參數后面一小節介紹);
- Set-Cookie設置了一個nonce的cookie,主要目的用於安全方面。
第2步:OIDC-Client - 認證請求
緊接上一步,瀏覽器在接收到第1步的302響應后,會對Location所指定的URL發起一個Get請求。這個請求攜帶的參數如下:
其中參數的含義在OIDC的認證請求有詳細的解釋(注:其中采用的認證類型不管是authorization code,或者implict,還是hybrid都無關緊要,它們的區別只是其適用場景的差異,並不影響整個流程)。
- client_id=implicit-client:發起認證請求的客戶端的唯一標識,這個客戶端事先已經在oidc-server.dev這個站點注冊過了。
- reponse_mode=form_post:指示oidc服務器應該使用form表單的形式返回數據給客戶端。
- response_type=id_token:區別於oauth2授權請求的一點,必須包含有id_token這一項。
- scope=openid profile:區別於oauth2授權請求的一點,必須包含有openid這一項。
- state:oauth2定義的一個狀態字符串,這里的實現是加密保存了一些客戶端的狀態信息(用於記錄客戶端的一些狀態,在登錄成功后會有用處),oidc會在認證完成后原樣返回這個參數。
- nonce:上一步中寫入cookie的值,這字符串將來會包含在idtoken中原樣返回給客戶端。
- redirect_uri:認證成功后的回調地址,oidc-server.dev會把認證的信息發送給這個地址。
第3步:OIDC-Server - 驗證請求信息
oidc-server.dev站點會驗證第2步中傳遞過來的信息,比如client_id是否有效,redircet_uri是否合法,其他的參數是否合法之類的驗證。如果驗證通過,則會進行下一步操作。
第4步:OIDC-Server - 打開登錄頁面
在oidc-server.dev站點驗證完成后,如果沒有從來沒有客戶端通過oidc-server.dev登陸過,那么第2步的請求會返回一個302重定向重定向到登錄頁面。如果是已經登錄,則會直接返回第5步中生產重定向地址。
瀏覽器會打開響應消息中Location指定的地址(登錄頁面)。如下:
第5步:OIDC-Server - 完成用戶登錄,同時記錄登錄狀態
在第四步輸入賬戶密碼點擊提交后,會POST如下信息到服務器端。
服務器驗證用戶的賬號密碼,通過后會使用Set-cookie維持自身的登錄狀態。然后使用302重定向到下一個頁面。
第6步:瀏覽器 - 打開上一步重定向的地址,同時自動發起一個post請求
form的地址是在第2步中設置的回調地址,form表單中包含(根據具體的認證方式authorization code,implict或者hybrid,其包含的信息會有一些差異,這個例子中是采用的implicit方式)如下信息:
- id_token:id_token即為認證的信息,OIDC的核心部分,采用JWT格式包裝的一個字符串。
- scope:用戶允許訪問的scope信息。
- state:第1步中發送的state,原樣返回。
- session_state:會話狀態。
id_token包含的具體的信息如下:
其中包含認證的服務器信息iss,客戶端的信息aud,時效信息nbf和exp,用戶信息sub和nickname,會話信息sid,以及第1步中設置的nonce。還有其簽名的信息alg=RS256,表示idtoken最后的一段信息(上圖中淺藍色的部分)是oidc-server.dev使用RSA256對id_token的header和payload部分所生產的數字簽名。客戶端需要使用oidc-server.dev提供的公鑰來驗證這個數字簽名。
第7步:OIDC-Client - 接收第6步POST過來的參數,構建自身的登錄狀態
客戶端驗證id_token的有效性,其中驗證所需的公鑰來自OIDC的發現服務中的jwk_uri,這個驗證是必須的,目的時為了保證客戶端得到的id_token是oidc-sercer.dev頒發的,並且沒有被篡改過,以及id_token的有效時間驗證。數字簽名的JWT可以保證id_token的不可否認性,認證和完整性,但是並不能保證其機密性,所以id_token中千萬不要包含有機密性要求的敏感的數據。如果確實需要包含,則需要對其進行加密處理(比如JWE規范)。其中驗證也包含對nonce(包含在id_token中)的驗證(第1步設置的名為nonce的cookie)。
在驗證完成后,客戶端就可以取出來其中包含的用戶信息來構建自身的登錄狀態,比如上如中Set-Cookie=lnh.oidc這個cookie。然后清除第1步中設置的名為nonce的cookie。
最后跳轉到客戶端指定的地址(這個地址信息被保存在第1步中傳遞給oidc-server.dev的state參數中,被oidc-server.dev原樣返回了這個信息)。然后讀取用戶信息如下(這里讀取的是id_token中的完整信息):
其他的客戶端登錄
登錄流程是和上面的步驟是一樣的,一樣會發起認證請求,區別在於已經登錄的時候會在第4步直接返回post信息給客戶端的地址,而不是打開一個登錄頁面,這里就不再詳細介紹了。大家可以在本地運行一下,通過fiddler觀察一下它們的請求流程。貼一下oidc-client-hybrid.dev這個客戶端登錄后的頁面吧:
統一退出
退出的流程相比登錄簡單一些。如下圖:
其中核心部分在於利用瀏覽器作為中間的媒介,來逐一的通知已經登錄的客戶端退出登錄。
第1步:OIDC-Client - 觸發登出請求
點擊Logout鏈接。
點擊退出后會觸發一個GET請求,如下:
上圖這個請求會返回一個302的響應,Location的地址指向oidc-server.dev的一個endsession的接口。同時會通過Set-Cookie來清除自身的cookie。
第2步:OIDC-Client - 登出請求
瀏覽器通過GET訪問上一步中指定的Location地址。
接口地址定義在OIDC的發現服務中的end_session_endpoint字段中,參數信息定義在OIDC的Front-Channel-Logout規范中。
第3步:OIDC-Server - 驗證登出請求
驗證上圖中傳遞的信息,如果信息無誤則再一次重定向到一個地址(這里是IdentityServer4的實現機制,其實可以無需這個再次重定向的)。
第4步:OIDC-Server - 登出自身,返回包含IFrame的HTML
瀏覽器打開第3步中重定向的地址:
響應中會通過Set-Cookie(idsrv和idsrv.session)清除oidc-server.dev自身的登錄狀態。然后包含一個HTML表單頁面,上圖中iframe指向的地址是IdentityServer4內部維持的一個地址。訪問這個地址后的信息如下:
1 <!DOCTYPE html> 2 <html> 3 <style>iframe{display:none;width:0;height:0;}</style> 4 <body> 5 <iframe src='http://oidc-client-implicit.dev/oidc/front-channel-logout-callback?sid=b51ea235574807beb0deff7c6db6a381&iss=http%3A%2F%2Foidc-server.dev'></iframe> 6 <iframe src='http://oidc-client-hybrid.dev/oidc/front-channel-logout-callback?sid=b51ea235574807beb0deff7c6db6a381&iss=http%3A%2F%2Foidc-server.dev'></iframe> 7 </body> 8 </html>
上面代碼中的iframe是真正的調用已經登錄的客戶端進行登出的地址(IdentityServer4會記錄下來已經登錄的客戶端,沒有登陸過的和沒有配置啟用Front-Channel-Logout的則不會出現在這里)。其中iframe指向的地址是OIDC客戶端在oidc-server.dev中注冊的時候配置的地址。參數則是動態附加上去的參數。
最后頁面中包含一個js腳本文件,在頁面load完成后,跳轉到第2步中指定的post_logout_redirect_uri指向的回調頁面。
第5步:OIDC-Client - 處理登出回調通知
在瀏覽器訪問上面代碼中iframe指向的地址的時候,被動登出的OIDC客戶端會接收到登出通知。
響應中通過Set-Cookie(lnh.oidc)清除了需要被動登出的客戶端的Cookie。至此,統一的登出完成。
總結
本文介紹了基於OIDC實現的SSO的工作原理和流程,但並未涉及到OIDC的具體實現IdentityServer4的是如何使用的(這部分通過讀我提供的源碼應該是很容易理解的),旨在解釋一下如何用OIDC實現SSO,而非如何使用OIDC的某一個實現框架。OIDC是一個協議族,這些具體每一步怎么做都是有標准的規范的,所以側重在了用HTTP來描述這個過程,這樣這個流程也就可以用在java,php,nodejs等等開發平台上。
參考
本文源代碼:https://github.com/linianhui/oidc.example
認證授權:http://www.cnblogs.com/linianhui/category/929878.html
Id Token:http://www.cnblogs.com/linianhui/p/openid-connect-core.html#auto_id_5
JWT:http://www.cnblogs.com/linianhui/p/oauth2-extensions-protocol-and-json-web-token.html#auto_id_5
數字簽名:http://www.cnblogs.com/linianhui/p/security-based-toolbox.html#auto_id_16
OIDC:http://openid.net/connect/
IdentityServer4:https://github.com/IdentityServer/IdentityServer4