前言
通常為了弄清楚一個概念,我們需要掌握十個概念。在判斷 JWT(JsonWebToken)
是否能代替 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引數
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
的流程是怎樣的:
-
首先需要向
Google API
註冊一個應用程式,註冊完畢之後就會拿到 認證資訊(credentials
)包括ID
和secret
。不是所有的程式型別都有secret
。 -
接下來就要向
Google
請求access token
。這裡先忽略一些細節,例如請求引數(當然需要上面申請到的secret
)。重要的是,如果你想訪問的是 使用者資源,這裡就會提醒使用者進行 授權。 -
如果 使用者授權 完畢。
Google
就會返回access token
。又或者是返回 授權程式碼(authorization code
),再通過程式碼取得access token
。
token
獲取到之後,就能夠帶上 token
訪問 API
了。
流程如下圖所示:
注意:在第三步通過
authorization code
兌換access token
的過程中,access token
,還會返回額外的資訊,這其中和之後更新相關的就是refresh token
。
一旦 access token
過期,你就可以通過 refresh token
再次請求 access token
。
以上只是大致的流程,並且故意省略了一些額外的概念。比如更新 access token
當然也可以不需要 refresh token
,這要根據你的 請求方式 和訪問的 資源型別 而定。
這裡又會引起另外的兩個問題:
-
如果
refesh token
也過期了怎麼辦?這時就需要使用者 重新登陸授權。 -
為什麼要區分
refresh token
和access 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
的流程圖,看圖說話:
-
還 未登陸 的使用者 開啟瀏覽器 訪問你的網站(
SP
),網站 提供服務 但是並 不負責使用者認證。 -
於是
SP
向IDP
傳送了一個SAML
認證請求,同時SP
將 使用者瀏覽器 重定向到IDP
。 -
IDP
在驗證完來自SP
的 請求無誤 之後,在瀏覽器中呈現 登陸表單 讓使用者填寫 使用者名稱 和 密碼 進行登陸。 -
一旦使用者登陸成功,
IDP
會生成一個包含 使用者資訊(使用者名稱 或者 密碼)的SAML token
(SAML token
又稱為SAML Assertion
,本質上是XML
節點)。IDP
向SP
返回token
,並且將 使用者重定向 到SP
(token
的返回是在 重定向步驟 中實現的,下面會詳細說明)。 -
SP
對拿到的token
進行驗證,並從中解析出 使用者資訊,例如 使用者是誰 以及 使用者的許可權 有哪些。此時就能夠根據這些資訊允許使用者訪問我們網站的內容。
當使用者在 IDP
登陸成功之後,IDP
需要將使用者 再次重定向 到 SP
站點,這一步通常有兩個辦法:
-
HTTP
重定向:這並不推薦,因為 重定向 的URL
長度 有限制,無法攜帶更長的資訊,比如SAML 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
下的 OAuth2.0
的流程。
-
使用者通過 客戶端(可以是 瀏覽器 也可以是 手機應用)想要訪問
SP
上的資源,但是SP
告訴使用者需要進行 認證,將使用者 重定向 至IDP
。 -
IDP
向 使用者 詢問SP
是否可以訪問 使用者資訊。如果使用者同意,IDP
向 客戶端 返回authorization code
。 -
客戶端拿到
authorization code
向IDP
交換access token
,並拿著access token
向SP
請求資源。 -
SP
接受到請求之後,拿著附帶的token
向IDP
驗證 使用者的身份。確認身份無誤後,SP
向 客戶端 發放相關資源。
那麼 OAuth
是如何避免 SAML
流程下 無法解析 POST
內容的資訊的呢?
-
一方面是使用者從
IDP
返回 客戶端 的方式,也是通過URL
重定向,這裡的URL
允許 自定義schema
,所以即使在 手機 上也能 拉起應用; -
另一方面因為
IDP
向 客戶端 傳遞的是authorization code
,而不是XML
資訊,所以code
可以很輕易的附著在 重定向URL
上進行傳遞。
但以上的 SSO
流程體現不出 OAuth
的本意。OAuth
的本意是 一個應用 允許 另一個應用 在 使用者授權 的情況下 訪問自己的資料。
OAuth
的設計本意更傾向於 授權而非認證(當然授權使用者資訊就間接實現了認證),雖然 Google
的 OAuth 2.0 API
同時支援 授權 和 認證。所以你在使用 Facebook
或者 Gmail
賬號登陸第三方站點時,會出現 授權對話方塊,告訴你 第三方站點 可以訪問你的哪些資訊,需要徵得你的同意。
在上面 SSO
的 OAuth
流程中涉及三方角色: SP
, IDP
以及 Client
。但在實際工作中 Client
可以是不存在的,例如你編寫了一個 後端程式 定時的通過 Google API
從 Youtube
拉取最新的節目資料,那麼你的 後端程式 需要得到 Youtube
的 OAuth
授權 即可。
OAuth VS OpenId
如果你有留心的話,你會在某些站點看到允許以 OpenID
的方式登陸,其實也就是以 Facebook
賬號或者 Google
賬號登陸站點:
OpenID
和 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
初步認識
本質上來說 JWT
也是 token
,正如我們在上文提到的,它是 訪問資源 的 憑證。
Google
的一些 API
諸如 Prediction API
或者 Google Cloud Storage
,是不需要 訪問 使用者的 個人資料 的。因而不需要經過 使用者的授權 這一步驟,應用程式可以直接訪問。就像上面 OAuth
中沒有 Client
沒有參與的流程類似。這就要藉助 JWT
完成訪問了, 具體流程如下:
-
首先需要在
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
,由三部分組成:
-
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
譯為 簽名,建立簽名要分以下幾個步驟:
-
從 介面服務端 拿到 金鑰,假設為
secret
。 -
對
header
進行base64
編碼,假設結果為headerStr
。 -
將
payload
進行base64
編碼,假設結果為payloadStr
。 -
將
headerStr
和payloadStr
用.
字元 拼裝起來成為字元data
。 -
以
data
和secret
作為引數,使用 雜湊演算法 計算出 簽名。
如果上述描述還不直觀,用 虛擬碼 表示就是:
// 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
) 的行為,用於保證 資料的完整性,實際上也並且並 沒有加密任何資料。
用於介面呼叫
接下來在 API
呼叫中就可以附上 JWT
(通常是在 HTTP Header
中)。又因為 SP
會與程式 共享 一個 secret
,所以 程式 可以通過 header
提供的相同的 hash
演算法來 驗證簽名 是否正確,從而判斷應用是否有權力呼叫 API
。
有狀態的對話Session
因為 HTTP
是 無狀態 的,所以 客戶端 和 服務端 需要解決的問題是,如何讓它們之間的對話變得有狀態。例如只有是 登陸狀態 的 使用者 才有許可權呼叫某些介面,那麼在 使用者登陸 之後,需要記住該使用者是 已經登陸 的狀態。常見的方法是使用 session
機制。
常見的 session
模型是這樣工作的:
-
使用者在瀏覽器 登陸 之後,服務端為使用者生成 唯一 的
session id
,儲存在 服務端 的 儲存服務(例如MySQL
,Redis
)中。 -
該
session id
也同時 返回給瀏覽器,以SESSION_ID
為KEY
儲存在瀏覽器的cookie
中。 -
如果使用者再次訪問該網站,
cookie
裡的SESSION_ID
會隨著 請求 一同發往 服務端。 -
服務端通過判斷
SESSION_ID
是否已經在Redis
中判斷使用者是否處於 登陸狀態。
相信你已經察覺了,理論上來說,JWT
機制可以取代 session
機制。使用者不需要提前進行登陸,後端也不需要 Redis
記錄使用者的登陸資訊。客戶端的本地儲存一份合法的 JWT
,當使用者需要呼叫介面時,附帶上該合法的 JWT
,每一次呼叫介面,後端都使用請求中附帶的 JWT
做一次 合法性的驗證。這樣也間接達到了 認證使用者 的目的。
然而 JWT
真的能取代 session
機制嗎?這麼做有哪些好處和壞處?這些問題將留在下一篇再討論。
歡迎關注技術公眾號: 零壹技術棧
本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。