不要用JWT替代session管理(上):全面了解Token,JWT,OAuth,SAML,SSO
來自專欄前端技術漫遊指南
通常為了弄清楚一個概念,我們需要掌握十個概念。在判斷 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.1Authorization: Bearer <token>Host: www.googleapis.com/// URL query string parameterGET https://www.googleapis.com/drive/v2/files?token=<token>// Python:from googleapiclient.discovery import builddrive = 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 了
流程如下圖所示:
注意在第三步通過 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 的流程圖,看圖說話
- 還未登陸的用戶打開瀏覽器訪問你的網站(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 的流程。
- 用戶通過客戶端(可以是瀏覽器也可以是手機應用)想要訪問 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 賬號登陸第三方站點時,會出授權對話框告訴你第三方站點可以訪問你的哪些信息,需要徵得你的同意:
在上面 SSO 的 OAuth 流程中涉及三方角色: SP, IdP 以及 Client。但在實際工作中 Client 可以是不存在的,例如你編寫了一個後端程序定時的通過 Google API 從 Youtube 拉取最新的節目數據,那麼你的後端程序需要得到 Youtube 的 OAuth 授權即可。
OAuth VS OpenId
如果你有留心的話,你會在某些站點看到允許以 OpenID 的方式登陸,其實也就是以 Facebook 賬號或者 Google 賬號登陸站點:
這聽上去似乎和 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
總結
這一小節我們重點了解了 OAuth,以及關於身份認證和授權的區別。現在我們可以把上一小節的知識關聯起來,也更加能理解 token:token 其實是為 OAuth 服務的,它是訪問數據的一把鑰匙。接下來我們看看這把鑰匙的另一種形態:Json Web Token, 簡稱 JWT
JWT
感性認識
首先我們需要從感性上認識 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,由三部分組成: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
- 將headerStr
和payloadStr
用.
字元串拼裝起來成為字元data
- 以data
和secret
作為參數,使用哈希演算法計算出簽名
如果上述描述還不直觀,用偽代碼表示就是:
// signature algorithmdata = 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 模型是這樣工作的:
- 用戶在瀏覽器登陸之後,服務端為用戶生成唯一的 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
https://developers.google.com/identity/protocols/OAuth2https://developers.google.com/identity/protocols/OAuth2ServiceAccount
JWT
https://medium.com/vandium-software/5-easy-steps-to-understanding-json-web-tokens-jwt-1164c0adfcecCookies vs. Tokens: The Definitive Guide - DZone Integrationhttps://auth0.com/blog/ten-things-you-should-know-about-tokens-and-cookies/https://stackoverflow.com/questions/39239051/rs256-vs-hs256-whats-the-difference
Refresh Token
What is the purpose of a "Refresh Token"?https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/Understanding Refresh Tokens - Auth0
Token VS Cookie
Cookies vs. Tokens: The Definitive Guide - DZone IntegrationToken Authentication vs. Cookies
Oauth
On a high level, how does OAuth 2 work?https://gist.github.com/mziwisky/10079157How does OAuth 2.0 work?An Introduction to OAuth 2
OpenID VS Oauth
Whats the difference between OpenID and OAuth?OAuth-OpenID: You』re Barking Up the Wrong Tree if you Think They』re the Same ThingAuthentication and Authorization: OpenID vs OAuth2 vs SAML
SAML VS Oauth
https://www.ubisecure.com/uncategorized/difference-between-saml-and-oauth/https://www.mutuallyhuman.com/blog/2013/05/09/choosing-an-sso-strategy-saml-vs-oauth2/Authentication and Authorization: OpenID vs OAuth2 vs SAML
推薦閱讀:
※奇舞周刊第 249 期:優雅的 Git Commit Message
※對TCP/IP模型的理解
※CSS 在relative absolute定位布局裡通過-margin定位技巧
※使用 Schematics 自定義 ng generate
※初級web前端工程師面試必看(HTML+CSS)