淺談SAML, OAuth, OpenID和SSO, JWT和Session

零壹技術棧發表於2018-07-05

前言

通常為了弄清楚一個概念,我們需要掌握十個概念。在判斷 JWT(JsonWebToken) 是否能代替 session 管理之前,我們要了解什麼是 token,以及 access tokenrefresh token 的區別。

瞭解什麼是 OAuth,什麼是 SSOSSO 下不同策略 OAuthSAML 的不同,以及 OAuthOpenID 的不同,更重要的是區分 authorisationauthentication

最後我們引出 JSON WEB TOKEN,聊聊 JWT 在 Session 管理方面的優勢和劣勢,同時嘗試解決這些劣勢,看看成本和代價有多少。

淺談SAML, OAuth, OpenID和SSO, JWT和Session

正文

本文關於 OAuth 授權API 呼叫例項都來自 Google API

關於Token

Token 即使是在計算機領域中也有不同的定義,這裡我們說的 token,是指 訪問資源 的憑據。例如當你呼叫 Google API 時,需要帶上有效 token 來表明你請求的 合法性。這個 TokenGoogle 給你的,這代表 Google 給你的 授權 使得你有能力訪問 API 背後的 資源

請求 API 時攜帶 token 的方式也有很多種,通過 HTTP Header 或者 url 引數或者 google 提供的類庫都可以:

  • HTTP Header
GET /drive/v2/files HTTP/1.1

Authorization: Bearer <token>
Host: www.googleapis.com/
複製程式碼
  • URL引數
GET https://www.googleapis.com/drive/v2/files?token=<token>
複製程式碼
  • Python函式庫
from googleapiclient.discovery import build
drive = build('drive', 'v2', credentials=credentials)
複製程式碼

更具體的說,上面用於呼叫 APItoken,我們稱為細分為 access token。通常 access token 是有 有效期限 的,如果 過期 就需要 重新獲取。那麼如何重新獲取?先看看第一次獲取 token 的流程是怎樣的:

  1. 首先需要向 Google API 註冊一個應用程式,註冊完畢之後就會拿到 認證資訊credentials)包括 IDsecret。不是所有的程式型別都有 secret

  2. 接下來就要向 Google 請求 access token。這裡先忽略一些細節,例如請求引數(當然需要上面申請到的 secret)。重要的是,如果你想訪問的是 使用者資源,這裡就會提醒使用者進行 授權

  3. 如果 使用者授權 完畢。Google 就會返回 access token。又或者是返回 授權程式碼authorization code),再通過程式碼取得 access token

token 獲取到之後,就能夠帶上 token 訪問 API 了。

流程如下圖所示:

淺談SAML, OAuth, OpenID和SSO, JWT和Session

注意:在第三步通過 authorization code 兌換 access token 的過程中,Google 並不會僅僅返回 access token,還會返回額外的資訊,這其中和之後更新相關的就是 refresh token

一旦 access token 過期,你就可以通過 refresh token 再次請求 access token

以上只是大致的流程,並且故意省略了一些額外的概念。比如更新 access token 當然也可以不需要 refresh token,這要根據你的 請求方式 和訪問的 資源型別 而定。

這裡又會引起另外的兩個問題:

  1. 如果 refesh token 也過期了怎麼辦?這時就需要使用者 重新登陸授權

  2. 為什麼要區分 refresh tokenaccess token?如果合併成一個 token 然後把 過期時間 調整的 更長,並且每次 失效 之後使用者 重新登陸授權 就好了?這個問題會和後面談的相關概念有關,後面會給予解釋說明。

OAuth 2.0

從獲取 token 到使用 token 訪問介面。這其實是標準的 OAuth2.0 機制下訪問 API 的流程。這裡介紹一下 OAuth 裡外相關的概念,更深入的理解 token 的作用。

SSO (Single sign-on)

通常公司內部會有非常多的平臺供大家使用,比如人力資源,程式碼管理,日誌監控,預算申請等等。如果每一個平臺都實現自己的使用者體系的話無疑是巨大的浪費,所以公司內部會有一套 公用的使用者體系,使用者只要登陸之後,就能夠 訪問所有的系統。這就是 單點登入

SSO 是一類 解決方案 的統稱,而在具體的實施方面,我們有兩種策略可供選擇:

  • SAML 2.0

  • OAuth 2.0

接下來我們區別這 兩種授權方式 有什麼不同。但是在描述 不同的策略 之前,我們先敘述幾個 共有的特性,並且相當重要的概念。

Authentication VS Authorisation

  • Authentication: 身份鑑別,以下簡稱 認證

  • Authorisation: 資源訪問 授權

認證 的作用在於 認可 你能夠訪問系統,用於 鑑別訪問者 是否是 合法使用者;而 授權 用於決定你有訪問 哪些資源的許可權

大多數人不會區分這兩者的區別,因為站在使用者的立場上。而作為系統的設計者來說,這兩者是有差別的,這是不同的兩個工作職責。我們可以只需要 認證功能,而不需要 授權功能,甚至不需要自己實現 認證功能。而藉助 Google 的認證系統,即使用者可以用 Google 的賬號進行登陸。

Authorization Server/Identity Provider(IdP)

把負責 認證的服務 稱為 AuthorizationServer 或者 IdentityProvider,以下簡稱 IDP

Service Provider(SP)/Resource Server

把負責 提供資源API 呼叫)的服務稱為 ResourceServer 或者 ServiceProvider,以下簡稱 SP

SAML 2.0

下圖是 SAML2.0 的流程圖,看圖說話:

淺談SAML, OAuth, OpenID和SSO, JWT和Session

  1. 未登陸 的使用者 開啟瀏覽器 訪問你的網站(SP),網站 提供服務 但是並 不負責使用者認證

  2. 於是 SPIDP 傳送了一個 SAML 認證請求,同時 SP使用者瀏覽器 重定向到 IDP

  3. IDP 在驗證完來自 SP請求無誤 之後,在瀏覽器中呈現 登陸表單 讓使用者填寫 使用者名稱密碼 進行登陸。

  4. 一旦使用者登陸成功, IDP 會生成一個包含 使用者資訊使用者名稱 或者 密碼)的 SAML tokenSAML token 又稱為 SAML Assertion,本質上是 XML 節點)。IDPSP 返回 token,並且將 使用者重定向SP (token 的返回是在 重定向步驟 中實現的,下面會詳細說明)。

  5. SP 對拿到的 token 進行驗證,並從中解析出 使用者資訊,例如 使用者是誰 以及 使用者的許可權 有哪些。此時就能夠根據這些資訊允許使用者訪問我們網站的內容。

當使用者在 IDP 登陸成功之後,IDP 需要將使用者 再次重定向SP 站點,這一步通常有兩個辦法:

  • HTTP 重定向:這並不推薦,因為 重定向URL 長度 有限制,無法攜帶更長的資訊,比如 SAML Token

  • HTTP POST 請求:這個是更常規的做法,當使用者登陸完畢之後渲染出一個表單,使用者點選後向 SP 提交 POST 請求。又或者可以使用 JavaScriptSP 發出一個 POST 請求。

如果你的應用是基於 Web,那麼以上的方案沒有任何問題。但如果你開發的是一個 iOS 或者 Android 的手機應用,那麼問題就來了:

  1. 使用者在 iPhone 上開啟應用,此時使用者需要通過 IDP 進行認證。

  2. 應用跳轉至 Safari 瀏覽器,在登陸認證完畢之後,需要通過 HTTP POST 的形式將 token 返回至 手機應用

雖然 POSTurl 可以 拉起應用,但是 手機應用 無法解析 POST 的內容,我們也就無法讀取 SAML Token

當然還是有辦法的,比如在 IDP 授權階段 不跳轉至系統的 Safari 瀏覽器,在 內嵌Webview 中解決,在想方設法從 Webview 中提取 token,或者利用 代理伺服器

無論如何,SAML 2.0不適用 於當下 跨平臺 的場景,這也許與它產生的年代也有關係,它誕生於 2005 年,在那個時刻 HTTP POST 確實是最好的選擇方案。

OAuth 2.0

我們先簡單瞭解 SSO 下的 OAuth2.0 的流程。

淺談SAML, OAuth, OpenID和SSO, JWT和Session

  1. 使用者通過 客戶端(可以是 瀏覽器 也可以是 手機應用)想要訪問 SP 上的資源,但是 SP 告訴使用者需要進行 認證,將使用者 重定向IDP

  2. IDP使用者 詢問 SP 是否可以訪問 使用者資訊。如果使用者同意,IDP客戶端 返回 authorization code

  3. 客戶端拿到 authorization codeIDP 交換 access token,並拿著 access tokenSP 請求資源。

  4. SP 接受到請求之後,拿著附帶的 tokenIDP 驗證 使用者的身份。確認身份無誤後,SP客戶端 發放相關資源。

那麼 OAuth 是如何避免 SAML 流程下 無法解析 POST 內容的資訊的呢?

  • 一方面是使用者從 IDP 返回 客戶端 的方式,也是通過 URL 重定向,這裡的 URL 允許 自定義 schema,所以即使在 手機 上也能 拉起應用

  • 另一方面因為 IDP客戶端 傳遞的是 authorization code,而不是 XML 資訊,所以 code 可以很輕易的附著在 重定向 URL 上進行傳遞。

但以上的 SSO 流程體現不出 OAuth 的本意。OAuth 的本意是 一個應用 允許 另一個應用使用者授權 的情況下 訪問自己的資料

OAuth 的設計本意更傾向於 授權而非認證(當然授權使用者資訊就間接實現了認證),雖然 GoogleOAuth 2.0 API 同時支援 授權認證。所以你在使用 Facebook 或者 Gmail 賬號登陸第三方站點時,會出現 授權對話方塊,告訴你 第三方站點 可以訪問你的哪些資訊,需要徵得你的同意。

淺談SAML, OAuth, OpenID和SSO, JWT和Session

在上面 SSOOAuth 流程中涉及三方角色: SP, IDP 以及 Client。但在實際工作中 Client 可以是不存在的,例如你編寫了一個 後端程式 定時的通過 Google APIYoutube 拉取最新的節目資料,那麼你的 後端程式 需要得到 YoutubeOAuth 授權 即可。

OAuth VS OpenId

如果你有留心的話,你會在某些站點看到允許以 OpenID 的方式登陸,其實也就是以 Facebook 賬號或者 Google 賬號登陸站點:

淺談SAML, OAuth, OpenID和SSO, JWT和Session

OpenIDOAuth 很像。但本質上來說它們是截然不同的兩個東西:

  • OpenID: 只用於 身份認證Authentication),允許你以 同一個賬戶多個網站登陸。它僅僅是為你的 合法身份 背書,當你以 Facebook 賬號登陸某個站點之後,該站點 無權訪問 你的在 Facebook 上的 資料

  • OAuth: 用於 授權Authorisation),允許 被授權方 訪問 授權方使用者資料

Refresh Token

現在可以回答上面的問題了,為什麼我們需要 refresh token

這樣的處理是為了 職責的分離

  • refresh token: 負責 身份認證

  • access token: 負責 請求資源

雖然 refresh tokenaccess token 都由 IDP 發出,但是 access token 還要和 SP 進行 資料交換,如果 公用的話 這樣就會有 身份洩露 的可能。並且 IDPSP 可能是 完全不同服務提供 的。而在上文,我們之所以沒有這樣的顧慮是因為 IDPSP 都是 Google

JWT

初步認識

本質上來說 JWT 也是 token,正如我們在上文提到的,它是 訪問資源憑證

Google 的一些 API 諸如 Prediction API 或者 Google Cloud Storage,是不需要 訪問 使用者的 個人資料 的。因而不需要經過 使用者的授權 這一步驟,應用程式可以直接訪問。就像上面 OAuth 中沒有 Client 沒有參與的流程類似。這就要藉助 JWT 完成訪問了, 具體流程如下:

淺談SAML, OAuth, OpenID和SSO, JWT和Session

  1. 首先需要在 Google API 上建立一個服務賬號(service account)。

  2. 獲取 服務賬號認證資訊credential),包括 郵箱地址client ID,以及一對 公鑰/私鑰

  3. 使用 Client ID私鑰 創一個 簽名JWT,然後將這個 JWT 傳送給 Google 交換 access token

  4. Google 返回 access token

  5. 程式通過 access token 訪問 API

甚至你可以不需要向 Google 索要 access token,而是攜帶 JWT 作為 HTTP header 裡的 bearer token 直接訪問 API 也是可以的。這才是 JWT 的最大魅力。

理性認識

JWT 顧名思義,它是 JSON 結構的 token,由三部分組成:

  • header

  • payload

  • signature

header

header 用於描述 元資訊,例如產生 signature 的演算法:

{    
    "typ": "JWT",
    "alg": "HS256"
}
複製程式碼

其中 alg 關鍵字就指定了使用哪一種 雜湊演算法 來建立 signature

payload

payload 用於攜帶你希望 向服務端傳遞 的資訊。你既可以往裡新增 官方欄位,例如:iss(Issuer), sub(Subject), exp(Expirationtime),也可以塞入 自定義的欄位,比如 userId:

{
    "userId": "b08f86af-35da-48f2-8fab-cef3904660bd"
}
複製程式碼

signature

signature 譯為 簽名,建立簽名要分以下幾個步驟:

  1. 介面服務端 拿到 金鑰,假設為 secret

  2. header 進行 base64 編碼,假設結果為 headerStr

  3. payload 進行 base64 編碼,假設結果為 payloadStr

  4. headerStrpayloadStr. 字元 拼裝起來成為字元 data

  5. datasecret 作為引數,使用 雜湊演算法 計算出 簽名

如果上述描述還不直觀,用 虛擬碼 表示就是:

// Signature algorithm
data = base64urlEncode( header ) + “.” + base64urlEncode( payload )
signature = Hash( data, secret );
複製程式碼

假設我們的原始 JSON 結構是這樣的:

// Header
{
    "typ": "JWT",
    "alg": "HS256"
}

// Payload
{
    "userId": "b08f86af-35da-48f2-8fab-cef3904660bd"
}
複製程式碼

如果 金鑰 是字串 secret 的話,那麼最終 JWT 的結果就是這樣的:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM
複製程式碼

可以在 jwt.io驗證 這個結果。

JWT究竟帶來了什麼

確保資料完整性

JWT 的目的不是為了 隱藏 或者 保密資料,而是為了確保 資料 確實來自被 授權的人 建立的,以防止 中途篡改

回想一下,當你拿到 JWT 時候,你完全可以在沒有 secret 的情況下解碼出 headerpayload,因為 headerpayload 只是經過了 base64 編碼(encode)而已,編碼的目的在於 利於資料結構的傳輸

雖然建立 signature 的過程近似於 加密 (encrypt),但本質其實是一種 簽名 (sign) 的行為,用於保證 資料的完整性,實際上也並且並 沒有加密任何資料

用於介面呼叫

接下來在 API 呼叫中就可以附上 JWT(通常是在 HTTP Header 中)。又因為 SP 會與程式 共享 一個 secret,所以 程式 可以通過 header 提供的相同的 hash 演算法來 驗證簽名 是否正確,從而判斷應用是否有權力呼叫 API

有狀態的對話Session

因為 HTTP無狀態 的,所以 客戶端服務端 需要解決的問題是,如何讓它們之間的對話變得有狀態。例如只有是 登陸狀態使用者 才有許可權呼叫某些介面,那麼在 使用者登陸 之後,需要記住該使用者是 已經登陸 的狀態。常見的方法是使用 session 機制。

常見的 session 模型是這樣工作的:

淺談SAML, OAuth, OpenID和SSO, JWT和Session

  1. 使用者在瀏覽器 登陸 之後,服務端為使用者生成 唯一session id,儲存在 服務端儲存服務(例如 MySQL, Redis)中。

  2. session id 也同時 返回給瀏覽器,以 SESSION_IDKEY 儲存在瀏覽器的 cookie 中。

  3. 如果使用者再次訪問該網站,cookie 裡的 SESSION_ID 會隨著 請求 一同發往 服務端

  4. 服務端通過判斷 SESSION_ID 是否已經在 Redis 中判斷使用者是否處於 登陸狀態

相信你已經察覺了,理論上來說,JWT 機制可以取代 session 機制。使用者不需要提前進行登陸,後端也不需要 Redis 記錄使用者的登陸資訊。客戶端的本地儲存一份合法的 JWT,當使用者需要呼叫介面時,附帶上該合法的 JWT,每一次呼叫介面,後端都使用請求中附帶的 JWT 做一次 合法性的驗證。這樣也間接達到了 認證使用者 的目的。

然而 JWT 真的能取代 session 機制嗎?這麼做有哪些好處和壞處?這些問題將留在下一篇再討論。


歡迎關注技術公眾號: 零壹技術棧

零壹技術棧

本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。

相關文章