一般爲了弄清楚一個概念,咱們須要掌握十個概念。在判斷 JWT(JsonWebToken)
是否能代替 session
管理以前,咱們要了解什麼是 token
,以及 access token
和 refresh token
的區別。python
瞭解什麼是 OAuth
,什麼是 SSO
,SSO
下不一樣策略 OAuth
和 SAML
的不一樣,以及 OAuth
與 OpenID
的不一樣,更重要的是區分 authorisation
和 authentication
。算法
最後咱們引出 JSON WEB TOKEN
,聊聊 JWT
在 Session
管理方面的優點和劣勢,同時嘗試解決這些劣勢,看當作本和代價有多少。編程
本文關於 OAuth
受權 和 API
調用實例都來自 Google API
。json
Token
即便是在計算機領域中也有不一樣的定義,這裏咱們說的 token
,是指 訪問資源 的憑據。例如當你調用 Google API
時,須要帶上有效 token
來代表你請求的 合法性。這個 Token
是 Google
給你的,這表明 Google
給你的 受權 使得你有能力訪問 API
背後的 資源。後端
請求 API
時攜帶 token
的方式也有不少種,經過 HTTP Header
或者 url
參數或者 google
提供的類庫均可以:api
GET /drive/v2/files HTTP/1.1
Authorization: Bearer <token>
Host: www.googleapis.com/
複製代碼
GET https://www.googleapis.com/drive/v2/files?token=<token>
複製代碼
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
)。重要的是,若是你想訪問的是 用戶資源,這裏就會提醒用戶進行 受權。bash
若是 用戶受權 完畢。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
而後把 過時時間 調整的 更長,而且每次 失效 以後用戶 從新登錄受權 就行了?這個問題會和後面談的相關概念有關,後面會給予解釋說明。
從獲取 token
到使用 token
訪問接口。這實際上是標準的 OAuth2.0
機制下訪問 API
的流程。這裏介紹一下 OAuth
裏外相關的概念,更深刻的理解 token
的做用。
一般公司內部會有很是多的平臺供你們使用,好比人力資源,代碼管理,日誌監控,預算申請等等。若是每個平臺都實現本身的用戶體系的話無疑是巨大的浪費,因此公司內部會有一套 公用的用戶體系,用戶只要登錄以後,就可以 訪問全部的系統。這就是 單點登陸。
SSO
是一類 解決方案 的統稱,而在具體的實施方面,咱們有兩種策略可供選擇:
SAML 2.0
OAuth 2.0
接下來咱們區別這 兩種受權方式 有什麼不一樣。可是在描述 不一樣的策略 以前,咱們先敘述幾個 共有的特性,而且至關重要的概念。
Authentication: 身份鑑別,如下簡稱 認證;
Authorisation: 資源訪問 受權。
認證 的做用在於 承認 你可以訪問系統,用於 鑑別訪問者 是不是 合法用戶;而 受權 用於決定你有訪問 哪些資源的權限。
大多數人不會區分這二者的區別,由於站在用戶的立場上。而做爲系統的設計者來講,這二者是有差異的,這是不一樣的兩個工做職責。咱們能夠只須要 認證功能,而不須要 受權功能,甚至不須要本身實現 認證功能。而藉助 Google
的認證系統,即用戶能夠用 Google
的帳號進行登錄。
把負責 認證的服務 稱爲 AuthorizationServer
或者 IdentityProvider
,如下簡稱 IDP
。
把負責 提供資源(API
調用)的服務稱爲 ResourceServer
或者 ServiceProvider
,如下簡稱 SP
。
下圖是 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
確實是最好的選擇方案。
咱們先簡單瞭解 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
受權 便可。
若是你有留心的話,你會在某些站點看到容許以 OpenID
的方式登錄,其實也就是以 Facebook
帳號或者 Google
帳號登錄站點:
OpenID
和 OAuth
很像。但本質上來講它們是大相徑庭的兩個東西:
OpenID: 只用於 身份認證(Authentication
),容許你以 同一個帳戶 在 多個網站登錄。它僅僅是爲你的 合法身份 背書,當你以 Facebook
帳號登錄某個站點以後,該站點 無權訪問 你的在 Facebook
上的 數據。
OAuth: 用於 受權(Authorisation
),容許 被受權方 訪問 受權方 的 用戶數據。
如今能夠回答上面的問題了,爲何咱們須要 refresh token
?
這樣的處理是爲了 職責的分離:
refresh token: 負責 身份認證;
access token: 負責 請求資源。
雖然 refresh token
和 access token
都由 IDP
發出,可是 access token
還要和 SP
進行 數據交換,若是 公用的話 這樣就會有 身份泄露 的可能。而且 IDP
和 SP
多是 徹底不一樣 的 服務提供 的。而在上文,咱們之因此沒有這樣的顧慮是由於 IDP
和 SP
都是 Google
。
本質上來講 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
用於描述 元信息,例如產生 signature
的算法:
{
"typ": "JWT",
"alg": "HS256"
}
複製代碼
其中 alg
關鍵字就指定了使用哪種 哈希算法 來建立 signature
。
payload
用於攜帶你但願 向服務端傳遞 的信息。你既能夠往裏添加 官方字段,例如:iss(Issuer)
, sub(Subject)
, exp(Expirationtime)
,也能夠塞入 自定義的字段,好比 userId
:
{
"userId": "b08f86af-35da-48f2-8fab-cef3904660bd"
}
複製代碼
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
時候,你徹底能夠在沒有 secret
的狀況下解碼出 header
和 payload
,由於 header
和 payload
只是通過了 base64
編碼(encode
)而已,編碼的目的在於 利於數據結構的傳輸。
雖然建立 signature
的過程近似於 加密 (encrypt
),但本質實際上是一種 簽名 (sign
) 的行爲,用於保證 數據的完整性,實際上也而且並 沒有加密任何數據。
接下來在 API
調用中就能夠附上 JWT
(一般是在 HTTP Header
中)。又由於 SP
會與程序 共享 一個 secret
,因此 程序 能夠經過 header
提供的相同的 hash
算法來 驗證簽名 是否正確,從而判斷應用是否有權力調用 API
。
由於 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
機制嗎?這麼作有哪些好處和壞處?這些問題將留在下一篇再討論。
歡迎關注技術公衆號: 零壹技術棧
本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。