在業務領域增多,規模擴大的情況下,單個系統已經無法滿足需求的變化,需要根據具體領域使用獨立系統完成功能。對於用戶來説就需要在這些獨立系統上分別進行登錄。在使用相同的賬號情況下,每個系統都登錄一次是多餘的,單點登錄的需求就應運而生。在使用單點登錄的情況下,在用戶在某個子系統 A 完成登錄后,一段時間内,用戶需要訪問子系統 B 就不需要再一次執行登錄操作了。
單點登錄最常見的實現方式使用 Central Authentication Service(CAS) 協議實現,其具體流程如下圖所示。
其實現方式將參與主體劃分爲了三個部分,包含瀏覽器,中心認證服務器,各種業務應用。用戶訪問應用是就會產生圖中的流程:
上述流程中涉及的登錄需要再詳細描述一下。當認證服務器發送需要登錄的信息給瀏覽器時,瀏覽器就會讓用戶進行認證操作,該操作可能包含輸入用戶名密碼或者手機號驗證碼或者其他認證手段。認證服務器根據這些信息確認了當前用戶身份。之後需要把對應的用戶信息存儲下來,這樣整個登錄過程就完成了。因此,應用發現用戶沒有登錄,其實是無法在某個存儲位置找到需要的用戶信息,從而向認證服務器發起重定向請求。這個存儲位置可以是任何地方,例如本地 session,請求擕帶的 cookie 等。
用戶信息在單機情況下存儲于服務器的 session 中,再在 cookie 中存儲 session key,從而用戶每次請求可以獲得對應信息。但是認證服務器多個實例進行部署時,基於服務器的 session 就完全無法滿足需求了。
相對妥協的方案可以讓相同用戶請求經過同一臺服務器,就不需要擔心服務器上沒有用戶信息。這樣的缺點在於和服務器強綁定,本臺服務器重啓用戶信息就會丟失,但是其實有其他服務器,這樣重新登錄未必合理。還要思考如何針對請求進行分流,確保同樣的用戶使用同一個服務器。一個改進是可以在服務器閒複製 session 數據,這樣每臺服務器都有登錄用戶信息了,不管用戶請求到哪一臺服務器,都可以找到用戶信息。這樣的方案涉及分佈式一致性問題,狀態難以管理,系統複雜度升高。假設用戶信息由中心化的單台服務器保存,就可以避免分佈式問題了。其它服務器需要用戶信息時即向該中心服務器請求用戶數據。此時所有服務器每次請求都要經過該中心服務器,導致其對應的流量增大,容易引發單點故障問題,從而整個系統不可用的風險也增大了。
以上都是在服務端存儲用戶信息的解決方案。如果轉變思路,把用戶信息存在客戶端會怎麽樣呢?這種思路的一個解決方案就是把用戶信息直接存儲于 cookie 中,每次需要獲取用戶信息時的請求,直接從 cookie 中把用戶信息讀出來即可。直接存儲的話可能會遭到篡改,受到攻擊。Json Web Token(JWT)就是在考慮方方面面的風險以後所產生的一個比較合理的解決方案。
至於在單點登錄系統中如何獲得及存儲用戶信息呢?一般有兩個方案。其一為將用戶信息存儲于 Cookie 中。在這個方案認證服務器和應用服務器需要用戶共同的頂級域名,然後認證服務器將用戶信息 Cookie 存儲于根路徑下,這樣就可以做到 Cookie 共享。應用查詢對應的 Cookie 即可獲得信息。這種方案限制也明顯,無法讓服務擁有不同的域名。
再一個方案即爲認證服務中登錄完成后,在重定向到應用服務時,附加一個 token 參數。應用服務接收到該 token,使用該 token 向認證服務請求從而獲得數據,之後隨應用服務方便將用戶信息存儲到合適的位置。來自同個用戶的請求就不需要登錄,從對應位置獲取即可。這種方案不僅解除了域名限制,同時還避免了多次向認證服務發請求產生的流量過大的危險。
參考第一節的流程圖,即可設計出以下的簡單接口,而完成整個單點登錄過程。省略認證服務中有關登錄的具體接口,這一部分可以自定義,完成登錄獲取用戶信息即可。對於重定向操作,完全是服務器請求瀏覽器重新調用對應服務的結果,并非服務之間調用。
這個接口是最初的起點,當用戶訪問應用服務,應用服務發現沒有登錄,即重定向到認證服務。
method: GET
參數: 隨業務需要而定
應用服務通過重定向該接口,造成用戶需要登陸認證服務。登錄完成後,認證服務進行必要的設置,重新重定向回應用服務。
method: GET
參數:
參數名 | 解釋 | 選擇 |
---|---|---|
returnURL | 應用服務的原來訪問地址,需要 base64 編碼,避免特殊符號帶來數據錯誤。 | 必選 |
當認證服務完成登錄重定向到應用服務時,應用服務需要能夠攔截到對應的請求,根據 Cookie 或發回來的 token 可以獲取用戶信息。在 java spring 中即可使用 AOP 或攔截器技術實現。此外應用服務獲取用戶信息后,放入 Cookie 中,對應的 token 也要保存下來。
瀏覽器在進行重定向時,會將 post 接口去除 body 轉化為 GET 請求,因此刻意將上述 Ⅰ、Ⅱ 接口設計成 GET,避免訪問時參數丟失。這樣也限制了請求類型,對於應用子系統來説,需要在用戶進入系統開始,便產生一個 GET 請求來獲取用戶信息,否則將阻礙後續 post 請求獲取用戶數據。
一個不是方案的解決方案是在重定向到認證服務時,將 post 的 body 參數和請求方法 method 拼接到 returnURL 參數上,從認證服務返回時,解析該參數,調用對應的服務即可。
從認證服務返回時,携帶的 token 會在瀏覽器地址欄中出現,這增加了用戶數據泄露的風險。這個解決方法可以參考 oauth2 的授權碼模式,認證服務返回一個存活期極少的授權碼 code,應用服務通過該 code 去向認證服務獲得訪問 token,再通過該 token 獲得用戶具體信息。
當用戶登錄信息在認證服務中失效,沒什麽好的辦法讓其在應用服務中同時失效。如果認證服務逐個通知應用服務,這樣數據量增大,同時也增加了系統複雜度,維護困難。最好的方法是讓應用服務主動失效,因此應用服務設計時需要將對應的 cookie 有效期設置低於認證服務中的有效期。
單點登錄簡化了用戶登錄過程,提升了用戶體驗。這些内部複雜的訪問過程,完全不需要用戶過度參與。信息系統大概就是對自動化,透明化的極緻追求吧。