登錄態 - SSO

1 背景

前一段時間,參與了老項目的遷移工作,配合後端接口遷移時,由於兩個項目採取了不一樣的登陸方案,所以遇到了跨域登錄態無法共享的問題。經過各方協調,最終老項目將遷移頁面部署在新項目的指定網關下,並且使用新項目的 SSO 登錄方案。由遷移中遇到的登陸態共享問題,引發了我對 SSO 的思考與學習。如果發現文章中有什麼錯誤之處,請及時指正~🙈

2 登錄態維護方式

2.1 JWT(Json Web Token)

2.1.1 是什麼?

JWT 是一個開放的 JSON 格式 token 存儲標準。它定義了一種安全、緊湊的方式來保存數據,通過簽名的方式來校驗 token 的合法性,主要支持的簽名算法如 HMAC、RSA、ECDSA。

通常使用它來保存登錄信息,相比傳統的 session 方案,它的優點在於服務端無需維護登錄態,不再需要依賴第三方存儲(如 redis、memcached),所以說 JWT 是無狀態的。

但它也存在缺點。由於它只在客戶端維護,因此服務端無法方便的清除登錄態,相比傳統的 session 方案,只需要將 session 清除即可。你可能會說,可以直接將這個 token 刪除就算退出登錄了。但實際上這只是一種假註銷,若該用戶再次拿到相同的 token 還是會被認爲是登錄的。

2.1.2 數據結構

實際上 JWT 是由header(頭部)payload(負載)signature(簽名)這三個部分組成的,中間用.來分隔開,寫成一行就是這個樣子的:Header.Payload.Signature

{

  "alg""HS256",  // 表示簽名的算法(algorithm),默認是 HMAC SHA256(寫成 HS256)

  "typ""JWT"     // 表示這個令牌(token)的類型(type),JWT 令牌默認統一寫爲 JWT

}
iss (issuer):簽發人

exp (expiration time):過期時間

sub (subject):主題

aud (audience):受衆

nbf (Not Before):生效時間

iat (Issued At):簽發時間

jti (JWT ID):編號

⚠️ JWT 默認是不加密的,任何人都可以讀到,所以不要把祕密信息放在這個部分。這個 JSON 對象也要使用 Base64URL 算法轉成字符串。

HMACSHA256(

  base64UrlEncode(header) + "." +

  base64UrlEncode(payload),

  secret)

2.1.3 特點

  1. JWT 默認是不加密,但也是可以加密的。JWT 不加密的情況下,不能將敏感數據寫入 JWT。但是,生成原始 Token 以後,可以用密鑰再加密一次。

  2. JWT 不僅可以用於認證,也可以用於交換信息。有效使用 JWT,可以降低服務器查詢數據庫的次數。

  3. JWT 的最大缺點是,由於服務器不保存 session 狀態,因此無法在使用過程中廢止某個 token,或者更改 token 的權限。也就是說,一旦 JWT 簽發了,在到期之前就會始終有效,除非服務器部署額外的邏輯。

  4. JWT 本身包含了認證信息,一旦泄露,任何人都可以獲得該令牌的所有權限。爲了減少盜用,JWT 的有效期應該設置得比較短,並且 JWT 不應該使用 HTTP 協議明碼傳輸,要使用 HTTPS 協議傳輸。對於一些比較重要的權限,使用時應該再次對用戶進行認證。

2.2.1 是什麼?

(1) Cookie

Cookie 是服務器發送到用戶瀏覽器並保存在本地的一小塊數據,它會在瀏覽器下次向同一服務器再發起請求時被攜帶併發送到服務器上。通常,它用於告知服務端兩個請求是否來自同一瀏覽器,如保持用戶的登錄狀態。Cookie 使基於無狀態的 HTTP 協議記錄穩定的狀態信息成爲了可能。

有兩種類型的 Cookie,一種是 Session Cookie(會話期 Cookie),一種是 Persistent Cookie(持久性 Cookie),如果 Cookie 不包含到期日期,則將其視爲會話 Cookie。會話 Cookie 存儲在內存中,永遠不會寫入磁盤,當瀏覽器關閉時,此後 Cookie 將永久丟失。如果 Cookie 包含有效期 ,則將其視爲持久性 Cookie,到期後,Cookie 將從磁盤中刪除。

主要用途:

(2)Session

Session 代表着服務器和客戶端一次會話的過程。Session 對象存儲特定用戶會話所需的屬性及配置信息。這樣,當用戶在應用程序的 Web 頁之間跳轉時,存儲在 Session 對象中的變量將不會丟失,而是在整個用戶會話中一直存在下去。

常見誤區:Session 不是關閉瀏覽器就消失了。對 Session 來說,除非程序通知服務器刪除一個 Session,否則服務器會在 Session 失效前一直保留。大多數情況下瀏覽器是不會在關閉網頁之前通知服務器的,所以服務器根本不知道瀏覽器已經關閉。之所以會有這種錯覺,是大部分 session 機制都使用會話 cookie 來保存 session id,而關閉瀏覽器後這個 session id 就消失了,再次連接服務器時也就無法找到原來的 session。如果服務器設置的 cookie 被保存在硬盤上,或者使用某種手段改寫瀏覽器發出的 HTTP 請求頭,把原來的 session id 發送給服務器,則再次打開瀏覽器仍然能夠打開原來的 session。

(1)關係

服務器第一次接收到請求時,開闢了一塊 Session 空間(創建了 Session 對象),同時生成一個 session id ,並通過響應頭的Set-Cookie:JSESSIONID=XXXXXXX 命令,向客戶端發送要求設置 Cookie 的響應;客戶端收到響應後,在本機客戶端設置了一個JSESSIONID=XXXXXXX的 Cookie 信息,該 Cookie 的過期時間爲瀏覽器會話結束。接下來客戶端每次向同一個網站發送請求時,請求頭都會帶上該 Cookie 信息(包含 sessionId ), 然後服務器通過讀取請求頭中的 Cookie 信息,獲取名稱爲 JSESSIONID 的值,得到此次請求的 session id。

(2)區別

  1. 安全性: 由於 Session 是存儲在服務器端的,Cookie 是存儲在客戶端的,所以 Cookie 被盜用的可能性相較於 Session 會更高一些。

  2. 存取值的類型不同:Cookie 只支持存字符串數據,想要設置其他類型的數據,需要將其轉換成字符串,Session 可以存任意數據類型。

  3. 存儲大小不同: 單個 Cookie 保存的數據不能超過 4K,Session 可存儲數據遠高於 Cookie,但是當訪問量過多,會佔用過多的服務器資源。

2.2.3 Session id 的攜帶方式

3 單點登錄 (SSO)

3.1 SSO 是什麼

SSO(Single sign-on)即單點登錄,一種對於許多相互關聯,但是又是各自獨立的軟件系統,提供訪問控制的方法。

單點登錄 (SSO) 發生在用戶登錄到一個應用程序,然後自動登錄到其他應用程序時,無論用戶使用何種平臺、技術或域。例如,如果你登錄 Gmail 等 Google 服務,會自動通過 YouTube、AdSense、Google Analytics 和其他 Google 應用程序的身份驗證。同樣,如果退出 Gmail 或其他 Google 應用程序,將自動退出所有應用程序;這稱爲單點註銷 (SLO)。

3.2 CAS

3.2.1 是什麼?

CAS(Central Authentication Service), 集中式認證服務, 是 Yale 大學發起的一個企業級的、開源的項目,旨在爲 Web 應用系統提供一種可靠的單點登錄解決方法 (屬於 Web SSO)。下圖來自維基百科。

SSO 體系中的角色

3.2.2 易混淆概念

TGT 是 CAS 爲用戶簽發的登錄票據,擁有了 TGT,用戶就可以證明自己在 CAS 成功登錄過。TGT 封裝了 Cookie 值以及此 Cookie 值對應的用戶信息。當 HTTP 請求到來時,CAS 以此 Cookie 值(TGC)爲 key 查詢緩存中有無 TGT ,如果有的話,則相信用戶已登錄過。

CAS Server 生成 TGT 放入自己的 Session 中,而 TGC 就是這個 Session 的唯一標識(SessionId),以 Cookie 形式放到瀏覽器端,是 CAS Server 用來明確用戶身份的憑證。

ST 是 CAS 爲用戶簽發的訪問某一 service 的票據。用戶訪問 service 時,service 發現用戶沒有 ST,則要求用戶去 CAS 獲取 ST。用戶向 CAS 發出獲取 ST 的請求,CAS 發現用戶有 TGT,則簽發一個 ST,返回給用戶。用戶拿着 ST 去訪問 service,service 拿 ST 去 CAS 驗證,驗證通過後,允許用戶訪問資源。ST 只能使用一次就會失效,這與 OAuth 中的 access_token 不同。

👇PGTIOU, PGT, PT CAS 2.0 的 代理模式 中的內容 ,感興趣的同學可以自行了解。

Proxy Service 的代理憑據。用戶通過 CAS 生成一個 PGT 對象,緩存在 PGTIOU。

是 CAS 的 serviceValidate 接口驗證 ST 成功後,CAS 會生成驗證 ST 成功的 xml 消息,返回給 Proxy Service,xml 消息中含有 PGTIOU,proxy service 收到 Xml 消息後,會從中解析出 PGTIOU 的值,然後以其爲 key,在 map 中找出 PGT 的值,賦值給代表用戶信息的 Assertion 對象的 pgtId,同時在 map 中將其刪除。

是用戶訪問 Target Service(back-end service)的票據。如果用戶訪問的是一個 Web 應用,則 Web 應用會要求瀏覽器提供 ST,瀏覽器就會用 cookie 去 CAS 獲取 ST,而是通過訪問 proxy service 的接口,憑藉 proxy service 的 PGT 去獲取一個 PT,然後才能訪問到此應用。

3.2.3 單點登錄 (SSO) & 單點登出 (SLO)

3.2.3.1 登陸流程

下圖是 CAS 官網的登錄時序圖,可以更好地幫助我們理解,建議細看一下~

3.2.3.2 單點登出

CAS 除了提供 SSO 功能,還提供了 SLO(單點登出)功能,由於 CAS Service 和 Client Service 各維護了一個登陸態,所以兩者之間的登錄態是割裂的,那我們應該怎麼實現 SLO 呢?

由於官方沒有給出一個詳細的流程圖,所以我就根據自己的理解畫了一個,供大家參考一下~

主要流程:

  1. 在某一個平臺請求退出登錄後,先在 query 攜帶 service 字段重定向到 CAS 退出登錄的頁面;

  2. 攜帶 service query 字段和 TGC 發送請求到 CAS Server;

  3. CAS Server 通過 TGC 查詢到登錄信息,然後遍歷請求各個接入平臺的/logout接口;

  4. 當所有業務的登錄態都清除後,就成功單點登出了。

3.3 OAUTH

3.3.1 是什麼?

在 OAuth 中 “O” 是 Open 的簡稱,表示 “開放” 的意思。Auth 表示 “授權” 的意思,所以連起來 OAuth 表示 “開放授權” 的意思,它是一個關於授權(authorization)的開放網絡標準。OAuth 允許用戶授權第三方應用訪問他存儲在另外服務商裏的各種信息數據,而這種授權不需要提供用戶名和密碼提供給第三方應用。比較直接的例子就是第三方 App 使用微信或 QQ 來登錄,這些授權登錄採用的就是 OAuth。

3.3.2 名詞定義

3.3.3 四種授權模式

本部分只展示一下相關的時序圖,就不做文檔的搬運工了🐶,想了解更多的同學,可以看最後的推薦閱讀部分~

  1. 授權碼模式

這種方式是最常用的流程,安全性也最高,它適用於那些有後端的 Web 應用。授權碼通過前端傳送,令牌則是儲存在後端,而且所有與資源服務器的通信都在後端完成。這樣的前後端分離,可以避免令牌泄漏。

  1. 簡化模式

第四步,直接返回 access_token 時,有被劫持的風險,所以 OAuth 採用如下的方式傳遞 token

https://a.com/callback#token=ACCESS_TOKEN

上面這個 URL,token參數就是令牌,客戶端在前端拿到令牌。注意,令牌的位置是 URL 錨點(fragment),而不是查詢字符串(querystring),這是因爲 OAuth 允許跳轉網址是 HTTP 協議,因此存在 "中間人攻擊" 的風險,而瀏覽器跳轉時,錨點不會發到服務器,就減少了泄漏令牌的風險。

這種方式把令牌直接傳給前端,是很不安全的。因此,只能用於一些安全要求不高的場景,並且令牌的有效期必須非常短,通常就是會話期間(session)有效,瀏覽器關掉,令牌就失效了。

  1. 密碼模式

採用這種方式不需要跳轉,而是把令牌放在 JSON 數據裏面,作爲 HTTP 響應,客戶端拿到令牌。這種方式需要用戶給出自己的用戶名 / 密碼,顯然風險很大,因此只適用於其他授權方式都無法採用的情況,而且必須是用戶高度信任的應用。

  1. 憑證模式、客戶端模式(client credentials

適用於沒有前端的命令行應用,即在命令行下請求令牌。這種方式給出的令牌,是針對第三方應用的,而不是針對用戶的,即有可能多個用戶共享同一個令牌。

3.3.4 令牌的種類與更新

每個發到 API 的請求,都必須帶有令牌。具體做法是在請求的頭信息,加上一個

Authorization字段,令牌就放在這個字段裏面。

服務提供商平臺頒發令牌的時候,一次性頒發兩個令牌,一個用於獲取數據的 access_token,另一個用於獲取新的令牌 refresh_token。access_token 的過期時間較短,refresh_token 的過期時間較長,當 access_token 過期了,就會使用 refresh_token 來請求新的 access_token。

通過 refresh_token 請求 access_token 的 url 示例

https://server.example.com/oauth/token?

grant_type``=refresh_token&

client_id``=CLIENT_ID&

client_secret``=CLIENT_SECRET&

refresh_token``=REFRESH_TOKEN

參數解釋

3.4 CAS 對比 OAUTH

  1. 設計初衷

  1. 安全性
  1. 資源存儲

4 實現一個簡單的 SSO

流程圖

首次登陸

非首次登陸

退出登錄

模塊

7fbViv

接口

GgGGgm

用戶平臺接口

ieB35U

SSO 平臺接口

gIWxBZ

頁面

nB5fXp

其他實現方案

前後端框架

本 demo 採用 eden monorepo 來組織共六個子項目,分別爲三個 React 前端子項目和三個 Node 後端子項目,後端採用的是公司內部封裝的 gulu 框架。

Token 持久化

由於 Node 後端需要記錄當前已登陸系統的用戶 token,所以本 Demo 採用 JSON 文件來暫時存儲已登錄用戶的 Token,使用 fs 來對 json 文件進行讀寫操作,模擬存儲和刪除 token 的過程。

本地端口

本次 demo 僅在本地運行展示,所有使用的均爲本地 localhost[1] 端口,端口對應如下

前後端分離跨域問題

由於只是 demo 演示項目,所以採用的是 config 文件中的代理來暫時解決。

devServer: {
      proxy: {
        '/api'{
          target: 'http://localhost:400x',
          changeOrigin: true,
          pathRewrite: { '^/api''' },
        },
      },
    },

5 問題討論

Q :JWT 如何註銷登錄?

A :由於 JWT 是無狀態的,服務端不存儲它,目前爲止還沒有了解到有什麼能夠不涉及到服務端存儲的註銷方式。

Q :業務方接入 SSO 系統,但是沒有實現維護登錄態的服務,會出現什麼問題?

A :SSO 系統,本質上仍然是 “授權” 服務, 即提供了集中式的授權管理, 但是 “鑑權” 應當業務方自己實現。例如:server 通過 token 獲取到了用戶的授權信息(user_id 之類的),如果業務方不把這個 “授權信息(登錄態)” 維護起來(session/JWT),那麼每次訪問都需要再走到 SSO Server,走一次完整流程。那麼就會導致以下兩個問題:1、對 SSO server 而言流量、請求會被放大;2、對用戶而言流程變長、響應時間變慢。

6 補充內容

6.1 加密算法

有興趣的可以看一下這篇文章:淺談常見的七種加密算法及實現 [2]

加密算法分對稱加密和非對稱加密,其中對稱加密算法的加密與解密密鑰相同,非對稱加密算法的加密密鑰與解密密鑰不同,此外,還有一類不需要密鑰的散列算法。常見的對稱加密算法主要有 DES3DESAES等,常見的 非對稱算法 主要有RSADSA等,散列算法 主要有SHA-1MD5等。

哈希算法的特點:

參考資料

[1] localhost: http://localhost

[2] 淺談常見的七種加密算法及實現: https://juejin.cn/post/6844903638117122056

[3] 傻傻分不清之 Cookie、Session、Token、JWT: https://juejin.cn/post/6844904034181070861#heading-17

[4] 前端需要了解的 SSO 與 CAS 知識: https://juejin.cn/post/6844903509272297480

[5] OAuth.0 原理淺析: https://juejin.cn/post/7010636081305485319

[6] 理解 OAuth 2.0: https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html

[7] OAuth 2.0 的四種方式: https://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_B_4YbKEsjNUd_hGDGHFAA