不要用JWT替代session管理(上):全面瞭解Token,JWT,OAuth,SAML,SSO

李熠發表於2018-07-03

通常為了弄清楚一個概念,我們需要掌握十個概念。在判斷 JWT (Json Web Token) 是否能代替 session 管理之前,我們要了解什麼是 token,以及 access token 和 refresh token 的區別;瞭解什麼是 OAuth,什麼是 SSO,SSO 下不同策略 OAuth 和 SAML 的不同,以及 OAuth 與 OpenID 的不同,更重要的是區分 authorisation 和 authentication;最後我們引出 JSON WEB TOKEN,聊聊 JWT 在 session 管理方面的優勢和劣勢,同時嘗試解決這些劣勢,看看成本和代價有多少

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

關於 Token

token 即使是在計算機領域中也有不同的定義,這裡我們說的token,是指訪問資源的憑據。例如當你呼叫Google API,需要帶上有效 token 來表明你請求的合法性。這個 token 是 Google 給你的,這代表 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 query string parameter
GET https://www.googleapis.com/drive/v2/files?token=<token>

// Python:
from googleapiclient.discovery import build
drive = build('drive', 'v2', credentials=credentials)
複製程式碼

更具體的說,上面用於呼叫 API 的 token 我們稱為細分為 access token。通常 access token 是有有效期限的,如果過期就需要重新獲取。那麼如何重新獲取?現在我們要讓時光倒流一會,回顧第一次獲取 token 的流程是怎樣的:

  1. 首先你需要向 Google API 註冊你的應用程式,註冊完畢之後你會拿到認證資訊(credentials)包括 ID 和 secret。不是所有的程式型別都有 secret。
  2. 接下來就要向 Google 請求 access token。這裡我們先忽略一些細節,例如請求引數(當然需要上面申請到的 secret)以及不同型別的程式的請求方式等。重要的是,如果你想訪問的是使用者資源,這裡就會提醒使用者進行授權。
  3. 如果使用者授權完畢。Google 就會返回 access token。又或者是返回授權程式碼(authorization code),你再通過程式碼取得 access token
  4. token 獲取到之後,就能夠帶上 token 訪問 API 了

流程如下圖所示:

不要用JWT替代session管理(上):全面瞭解Token,JWT,OAuth,SAML,SSO

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

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

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

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

  1. 如果 refesh token 也過期了怎麼辦?這就需要使用者重新登陸授權了
  2. 為什麼要區分 refresh token 和 access token ?如果合併成一個 token 然後把過期時間調整的更長,並且每次失效之後使用者重新登陸授權就好了?這個問題會和後面談的相關概念有關,稍後再回答

OAuth

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

SSO (Single sign-on)

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

SSO 是一類解決方案的統稱,而在具體的實施方面,我們有兩種策略可供選擇:1) SAML 2.0 ; 2) OAuth 2.0。接下來我們區別這兩種授權方式有什麼不同。

但是在描述不同的策略之前,我們先敘述幾個共有的,並且相當重要的概念。

Authentication VS Authorisation

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

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

Authorization Server/Identity Provider(IdP) VS Service Provider(SP)/Resource Server

把負責認證的服務稱為 Authorization Server 或者 Identity Provider,以下簡稱 IdP;而負責提供資源(API呼叫)的服務稱為 Resource Server 或者 Service Provider,以下簡稱 SP

SMAL 2.0

下圖是 SMAL 2.0 的流程圖,看圖說話

不要用JWT替代session管理(上):全面瞭解Token,JWT,OAuth,SAML,SSO

  • 還未登陸的使用者開啟瀏覽器訪問你的網站(SP,以下都簡稱 SP),網站提供服務但是並不負責使用者認證。
  • 於是 SP 向 IdP 傳送了一個 SAML 認證請求,同時 SP 將使用者瀏覽器重定向到 IdP 。
  • IdP 在驗證完來自 SAML 的請求無誤之後,在瀏覽器中呈現登陸表單讓使用者進行填寫使用者名稱和密碼進行登陸
  • 一旦使用者登陸成功,IdP 會生成一個包含使用者資訊(使用者名稱或者密碼)的 SAML token (SAML token 又稱為 SAML Assertion,本質上是 XML 節點),IdP 向 SP 返回 token, 並且將使用者重定向到 SP (token 的返回是在重定向步驟中實現的,下面會詳細說明)
  • SP 對拿到的 token 進行驗證,並從中解析出使用者資訊,例如他們是誰以及他們的許可權有哪些。此時就能夠根據這些資訊允許使用者訪問我們網站的內容了

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

  • HTTP 重定向(HTTP Redirect):這並不推薦,應為重定向的 URL 長度有限,無法攜帶更長的資訊,比如 SMAL Token
  • HTTP POST 請求:這個是更常規的做法,當使用者登陸完畢之後渲染出一個表單,使用者點選後向 SP 提交 POST 請求。又或者可以使用 Javascript 向 SP 發出一個 POST 請求

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

  • 使用者在 iPhone 上開啟應用,此時使用者需要通過 IdP 進行認證
  • 應用跳轉至 Safari 瀏覽器,在登陸認證完畢之後,需要通過 HTTP POST 的形式將 token 返回至手機應用

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

當然還是有辦法的,比如在 IdP 授權階段不跳轉至系統的 Safari 瀏覽器,在內嵌的 webview 中解決,在想方設法從 webview 中提取 token,或者利用代理伺服器。但無論如何,SAML 2.0 並不適用於當下跨平臺的場景,這也許與它產生的年代也有關係,它誕生於 2005 年,在那個時刻 HTTP POST 確實是最好的選擇方案

OAuth 2.0

我們先簡單瞭解 SSO 下的 OAuth 2.0 的流程。

不要用JWT替代session管理(上):全面瞭解Token,JWT,OAuth,SAML,SSO

  • 使用者通過客戶端(可以是瀏覽器也可以是手機應用)想要訪問 SP 上的資源,但是 SP 告訴使用者需要進行認證,將使用者重定向至 IdP
  • IdP 向使用者詢問 SP 是否可以訪問使用者資訊,如果使用者同意,IdP 向客戶端返回 access code
  • 客戶端拿 code 向 IdP 換 access token,並拿著 access token 向 SP 請求資源
  • SP 接受到請求之後拿著附帶 token 向 IdP 驗證使用者的身份

那麼 OAuth 是如何避免 SAML 流程下無法解析 POST 內容的資訊的呢?使用者從 IdP 返回客戶端的方式是通過 URL 重定向,這裡的 URL 允許自定義schema,所以即使在手機上也能拉起應用;另一方面因為 IdP 向客戶端傳遞的是 code,而不是 XML 資訊,所以 code 可以很輕易的附著在重定向 URL 上進行傳遞

但以上的 SSO 流程體現不出 OAuth 的本意。OAuth 的本意是一個應用允許另一個應用在使用者授權的情況下訪問自己的資料,OAuth 的設計本意更傾向於授權而非認證(當然授權使用者資訊就間接實現了認證), 雖然 Google 的 OAuth 2.0 API 同時支援授權和認證。所以你在使用 Facebook 或者 Gmail 賬號登陸第三方站點時,會出授權對話方塊告訴你第三方站點可以訪問你的哪些資訊,需要徵得你的同意:

不要用JWT替代session管理(上):全面瞭解Token,JWT,OAuth,SAML,SSO

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

OAuth VS OpenId

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

不要用JWT替代session管理(上):全面瞭解Token,JWT,OAuth,SAML,SSO

這聽上去似乎和 OAuth 很像。但本質上來說它們是截然不同使用者的兩個東西:

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

Refresh Token

現在我們可以回答本篇第一小節的那個問題了:為什麼我們需要 refresh token?

這樣的處理是為了職責的分離:refresh token 負責身份認證,access token 負責請求資源。雖然 refresh token 和 access token 都由 IdP 發出,但是 access token 還要和 SP 進行資料交換,如果公用的話這樣就會有身份洩露的可能。並且 IdP 和 SP 可能是完全不同的服務提供的。而在第一小節中我們之所以沒有這樣的顧慮是因為 IdP 和 SP 都是 Google

不要用JWT替代session管理(上):全面瞭解Token,JWT,OAuth,SAML,SSO

總結

這一小節我們重點了解了 OAuth,以及關於身份認證和授權的區別。現在我們可以把上一小節的知識關聯起來,也更加能理解 token:token 其實是為 OAuth 服務的,它是訪問資料的一把鑰匙。接下來我們看看這把鑰匙的另一種形態:Json Web Token, 簡稱 JWT

JWT

感性認識

首先我們需要從感性上認識 JWT。本質上來說 JWT 也是 token,正如我們第一小節學習到的,它是訪問資源的憑證

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

不要用JWT替代session管理(上):全面瞭解Token,JWT,OAuth,SAML,SSO

  • 首先你需要再 Google API 上建立一個服務賬號(service account)
  • 獲取服務賬號的認證資訊(credential),包括郵箱地址,client ID,以及一對公鑰/私鑰
  • 使用 client ID 和私鑰創一個簽名的 JWT,然後將這個 JWT 傳送給 Google 交換 access token
  • Google 返回 access token
  • 程式通過 access token 訪問 API

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

理性認識

JWT 顧名思義,它是 JSON 結構的 token,由三部分組成:1) header 2) payload 3) signature

header

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

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

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

payload

payload 用於攜帶你希望向服務端傳遞的資訊。你既可以往裡新增官方欄位(這裡的“欄位” (field) 也可以被稱作“宣告” claims),例如iss(Issuer), sub(Subject), exp(Expiration time),也可以塞入自定義的欄位,比如 userId:

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

signature

signature 譯為「簽名」

建立簽名要分以下幾個步驟:

  • 你需要從介面服務端拿到金鑰,假設為secret
  • header進行 base64 編碼,假設結果為headerStr
  • payload進行 base64 編碼,假設結果為payloadStr
  • headerStrpayloadStr.字串拼裝起來成為字元data
  • 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 的情況下解碼出 header 和 payload,因為 header 和 payload 只是經過了 base64 編碼(encode)而已,編碼的目的在於利於資料結構的傳輸。雖然建立 signature 的過程近似於加密 (encrypt),但本質其實是一種簽名 (sign) 的行為,用於保證資料的完整性,實際上也並且並沒有加密任何資料

關於 Encoding, Encryption, Hashing之間的差異,可以參考這篇文章:Encoding vs. Encryption vs. Hashing vs. Obfuscation

用於介面呼叫

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

有狀態的對話

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

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

不要用JWT替代session管理(上):全面瞭解Token,JWT,OAuth,SAML,SSO

  • 使用者在瀏覽器登陸之後,服務端為使用者生成唯一的 session id,儲存在服務端的儲存服務(例如 MySql, Redis)中
  • 該 session id 也同時返回給瀏覽器,以 SESSION_ID 為 KEY 儲存在瀏覽器的 cookie 中
  • 如果使用者再次訪問該網站,cookie 裡的 SESSION_ID 會隨著請求一同發往服務端
  • 服務端通過判斷 SESSION_ID 是否已經在 Redis 中判斷使用者是否處於登陸狀態

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

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

本文也同時發表在我的知乎專欄上,歡迎大家關注

參考資料

Google API

JWT

Refresh Token

Token VS Cookie

Oauth

OpenID VS Oauth

SAML VS Oauth

相關文章