厲害了!單點登錄系統用 8 張漫畫就解釋了。。。

JSON Web Token(JWT)是一個非常輕巧的規範。這個規範允許我們使用 JWT 在用戶和服務器之間傳遞安全可靠的信息。

讓我們來假想一下一個場景。在 A 用戶關注了 B 用戶的時候,系統發郵件給 B 用戶,並且附有一個鏈接 “點此關注 A 用戶”。鏈接的地址可以是這樣的

https://your.awesome-app.com/make-friend/?from_user=B&target_user=A

上面的 URL 主要通過 URL 來描述這個當然這樣做有一個弊端,那就是要求用戶 B 用戶是一定要先登錄的。可不可以簡化這個流程,讓 B 用戶不用登錄就可以完成這個操作。JWT 就允許我們做到這點。

JWT 的組成

載荷(Payload)

{
    "iss""John Wu JWT",
    "iat": 1441593502,
    "exp": 1441594722,
    "aud""www.example.com",
    "sub""jrocket@example.com",
    "from_user""B",
    "target_user""A"
}

這裏面的前五個字段都是由 JWT 的標準所定義的。

這些定義都可以在標準中找到。

將上面的 JSON 對象進行 [base64 編碼] 可以得到下面的字符串。這個字符串我們將它稱作 JWT 的 Payload(載荷)。

eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdW

如果你使用 Node.js,可以用 Node.js 的包 base64url 來得到這個字符串。

小知識:Base64 是一種編碼,也就是說,它是可以被翻譯回原來的樣子來的。它並不是一種加密過程。

var base64url = require('base64url')
var header = {
    "from_user""B",
    "target_user""A"
}
console.log(base64url(JSON.stringify(header)))
// 輸出:eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9

JWT 還需要一個頭部,頭部用於描述關於該 JWT 的最基本的信息,例如其類型以及簽名所用的算法等。這也可以被表示成一個 JSON 對象。

{
  "typ""JWT",
  "alg""HS256"
}

在這裏,我們說明了這是一個 JWT,並且我們所用的簽名算法(後面會提到)是 HS256 算法。

對它也要進行 Base64 編碼,之後的字符串就成了 JWT 的 Header(頭部)。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

簽名(簽名)

將上面的兩個編碼後的字符串都用句號. 連接在一起(頭部在前),就形成了

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0

這一部分的過程在 node-jws 的源碼中有體現

最後,我們將上面拼接完的字符串用 HS256 算法進行加密。在加密的時候,我們還需要提供一個密鑰(secret)。如果我們用 mystar 作爲密鑰的話,那麼就可以得到我們加密後的內容

rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

這一部分又叫做簽名

最後將這一部分簽名也拼接在被簽名的字符串後面,我們就得到了完整的 JWT

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn

於是,我們就可以將郵件中的 URL 改成

https://your.awesome-app.com/make-friend/?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJm

這樣就可以安全地完成添加好友的操作了!

且慢,我們一定會有一些問題:

  1. 簽名的目的是什麼?

  2. Base64 是一種編碼,是可逆的,那麼我的信息不就被暴露了嗎?

讓我逐一爲你說明。

簽名的目的

最後一步簽名的過程,實際上是對頭部以及載荷內容進行簽名。一般而言,加密算法對於不同的輸入產生的輸出總是不一樣的。對於兩個不同的輸入,產生同樣的輸出的概率極其地小(有可能比我成世界首富的概率還小)。所以,我們就把 “不一樣的輸入產生不一樣的輸出” 當做必然事件來看待吧。

所以,如果有人對頭部以及載荷的內容解碼之後進行修改,再進行編碼的話,那麼新的頭部和載荷的簽名和之前的簽名就將是不一樣的。而且,如果不知道服務器加密的時候用的密鑰的話,得出來的簽名也一定會是不一樣的。

信息會暴露?

是的。

所以,在 JWT 中,不應該在載荷裏面加入任何敏感的數據。在上面的例子中,我們傳輸的是用戶的 User ID。這個值實際上不是什麼敏感內容,一般情況下被知道也是安全的。

但是像密碼這樣的內容就不能被放在 JWT 中了。如果將用戶的密碼放在了 JWT 中,那麼懷有惡意的第三方通過 Base64 解碼就能很快地知道你的密碼了。

JWT 的適用場景

我們可以看到,JWT 適合用於向 Web 應用傳遞一些非敏感信息。例如在上面提到的完成加好友的操作,還有諸如下訂單的操作等等。

用戶認證八步走

所謂用戶認證(Authentication),就是讓用戶登錄,並且在接下來的一段時間內讓用戶訪問網站時可以使用其賬戶,而不需要再次登錄的機制。

小知識:可別把用戶認證和用戶授權(Authorization)搞混了。用戶授權指的是規定並允許用戶使用自己的權限,例如發佈帖子、管理站點等。

首先,服務器應用(下面簡稱 “應用”)讓用戶通過 Web 表單將自己的用戶名和密碼發送到服務器的接口。這一過程一般是一個 HTTP POST 請求。建議的方式是通過 SSL 加密的傳輸(https 協議),從而避免敏感信息被嗅探。

接下來,應用和數據庫覈對用戶名和密碼。

覈對用戶名和密碼成功後,應用將用戶的 id(圖中的 user_id)作爲 JWT Payload 的一個屬性,將其與頭部分別進行 Base64 編碼拼接後簽名,形成一個 JWT。這裏的 JWT 就是一個形同 lll.zzz.xxx 的字符串。

應用將 JWT 字符串作爲該請求 Cookie 的一部分返回給用戶。注意,在這裏必須使用 HttpOnly 屬性來防止 Cookie 被 JavaScript 讀取,從而避免跨站腳本攻擊(XSS 攻擊)。

在 Cookie 失效或者被刪除前,用戶每次訪問應用,應用都會接受到含有 jwt 的 Cookie。從而應用就可以將 JWT 從請求中提取出來。

應用通過一系列任務檢查 JWT 的有效性。例如,檢查簽名是否正確;檢查 Token 是否過期;檢查 Token 的接收方是否是自己(可選)。

應用在確認 JWT 有效之後,JWT 進行 Base64 解碼(可能在上一步中已經完成),然後在 Payload 中讀取用戶的 id 值,也就是 user_id 屬性。這裏用戶的 id 爲 1025。

應用從數據庫取到 id 爲 1025 的用戶的信息,加載到內存中,進行 ORM 之類的一系列底層邏輯初始化。

應用根據用戶請求進行響應。

和 Session 方式存儲 id 的差異

Session 方式存儲用戶 id 的最大弊病在於要佔用大量服務器內存,對於較大型應用而言可能還要保存許多的狀態。一般而言,大型應用還需要藉助一些 KV 數據庫和一系列緩存機制來實現 Session 的存儲。

而 JWT 方式將用戶狀態分散到了客戶端中,可以明顯減輕服務端的內存壓力。除了用戶 id 之外,還可以存儲其他的和用戶相關的信息,例如該用戶是否是管理員、用戶所在的分桶(見 [《你所應該知道的 A/B 測試基礎》一文]( /2015/08/27/introduction-to-ab-testing/)等。

雖說 JWT 方式讓服務器有一些計算壓力(例如加密、編碼和解碼),但是這些壓力相比磁盤 I/O 而言或許是半斤八兩。具體是否採用,需要在不同場景下用數據說話。

單點登錄

Session 方式來存儲用戶 id,一開始用戶的 Session 只會存儲在一臺服務器上。對於有多個子域名的站點,每個子域名至少會對應一臺不同的服務器,例如:

所以如果要實現在 login.taobao.com 登錄後,在其他的子域名下依然可以取到 Session,這要求我們在多臺服務器上同步 Session。

使用 JWT 的方式則沒有這個問題的存在,因爲用戶的狀態已經被傳送到了客戶端。因此,我們只需要將含有 JWT 的 Cookie 的 domain 設置爲頂級域名即可,例如

Set-Cookie: jwt=lll.zzz.xxx; HttpOnly; max-age=980000; domain=.taobao.com

注意 domain 必須設置爲一個點加頂級域名,即. taobao.com。這樣,taobao.com 和 *.taobao.com 就都可以接受到這個 Cookie,並獲取 JWT 了。

原文:https://u.nu/2k4wk

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/MYP51qKPq0LT_AD9QJFSKA