淺顯易懂講解如何用JWT來加固API
- 2019 年 12 月 13 日
- 筆記
您一定聽說過JSON Web Token(JWT)吧? 它是當前用來保護API的先進技術之一。與大多數安全概念與技術一樣,我們在準備使用它之前,了解其工作原理是非常必要且重要的。當然,過於專業和技術性的JWT解釋可能會讓您覺得費解,甚至感到頭痛。那麼讓我試著用一種比較淺顯易懂的方式,向您闡述JWT是如何加固API的吧。

API身份驗證
不言而喻,在複雜的網路環境中,我們需要對各種API資源實施訪問限制。例如,我們不希望某個用戶能夠更改另一個用戶的密碼。那麼,我們就需要該用戶以提交其ID和密碼的方式,來保護和加固目標資源。換句話說:我們需要對他們進行身份驗證。
而在實際應用中,我們保護HTTP類API的難點在於:各種請求是無狀態的。也就是說:API無法知道任意兩個請求是否來自同一個用戶。有人可能會追問:我們為什麼不能要求用戶在每次調用API時,都提供他們的ID和密碼呢?答案是:因為這樣會給用戶帶來極差的訪問體驗。
JSON Web Token
因此,我們需要的是:用戶只用一次性提供信任憑據,而在後續的請求中,伺服器會以另一種方式進行用戶身份的識別。基於這種思想,JSON Web Token應運而生。
當然,如果您是一位愛好鑽研的學霸,那麼您可以通過鏈接:https://robmclarty.com/blog/what-is-a-json-web-token,來對JSON Web Token的工作原理進行全面深入的參悟。
在此讓我們想像一下:如果您打算入住一家酒店,那麼「令牌」就是允許您進入自己房間、以及酒店內其他設施的安全門卡,顯然您不能進入其他人的房間。而且在退房的時候,您需要退還門卡,即:註銷。
令牌的結構
通常情況下, JSON Web Token是通過各種HTTP請求的頭部(header)被發送的。如下所示:
Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
可見,令牌就是"Authorization:Bearer"的後面部分,隸屬於HTTP的頭部資訊。
上述資訊雖然顯得比較凌亂,不過它包含了如下部分:
首先,令牌由三個不同的字元串所組成,它們分別以點號隔開。這三個字元串使用了base 64編碼,分別對應頭部(header)、有效載荷(payload)和簽名(signature),如下所示:
// Header eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 // Payload eyJzdWIiOiIxMjM0NTY3ODkwIn0 // Signature dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
註:base 64是一種轉換字元串的方法,能夠確保字元串在跨Web傳輸的過程中不會出現問題。由於它不是一種加密方法,因此任何人都可以很容易地對它進行解碼,以查看原始數據。
下面,我們對上述字元串進行解碼,以便更好地了解JWT的結構。
頭部
通過解碼令牌的頭部,我們可以得到如下的元資訊(meta information)。由於它對於我們理解整體的工作原理幫助不大,所以我們在此並不做詳細解讀。
{ "alg":"HS256", "typ":"JWT" } 有效載荷
有效載荷里的內容要豐富得多。您可以用它來包含任何自己需要傳遞的數據。在此,由於該令牌的目的是對API的訪問進行身份驗證,因此僅包含了用戶的ID。
{ "userId":"1234567890" } 值得注意的是:有效負載並不安全。任何人都可以通過解碼令牌,來查看有效負載中的確切內容。因此,我們通常只包含ID,而不會包含諸如用戶郵件內容等敏感的標識資訊。
儘管該有效負載為API提供了識別用戶所需的全部資訊,但是它並不提供具體的身份驗證方法。畢竟憑藉這些資訊,黑客足以能夠輕鬆地找到用戶的ID,並可偽造出令牌。因此,我們還需要有簽名,而它才是令牌認證環節中的關鍵部分。
哈希演算法
在開始解釋簽名的工作原理之前,我們需要先來了解一下什麼是哈希演算法。
首先,它是一個函數,可用來將目標字元串轉換為另一種被稱為哈希值(hash)的新字元串。例如,我們對字元串「Hello, world.」進行哈希操作,那麼就能夠得到如下經過了SHA256哈希演算法的輸出:
4ae7c3b6ac0beff671efa8cf57386151c06e58ca53a78d83f36107316cec125f
註:哈希演算法有許多種不同的類型,JWT常用的是SHA256。
而哈希的重要屬性在於:我們無法使用哈希演算法,通過哈希值來識別出原始的字元串。換句話說,我們無法憑藉上述哈希值,直接計算或得出原始的字元串「Hello, world.」。從理論上說,根據哈希的複雜性,猜測出原始字元串是完全不可行的。
JWT簽名
現在,讓我們來看JWT令牌結構的第三個部分:簽名。實際上,該部分是需要進行計算的。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), "secret string" );
我們下面來具體分析一下上述程式碼:
- 首先,HMACSHA256是哈希函數的名稱,它用到了兩個參數:需要進行哈希的字元串和密鑰(secret)。
- 其次,這個需要進行哈希的字元串,是經過base 64編碼過的頭部和有效載荷。
- 第三,密鑰是一串任意數據,而且只有伺服器知曉。
問:為什麼要將頭部和有效載荷添加到簽名的哈希值中呢?
答:這樣可以確保簽名對於該特定令牌來說是僅有的。
問:什麼是密鑰?
答:讓我們從如何偽造一個令牌的角度來回答該問題。我們之前說過,黑客無法從輸出值來推導出經過哈希的輸入資訊。但是,由於簽名中包括了頭部和有效載荷,而這些都是公共的資訊,因此如果黑客知道了哈希演算法(這通常是在頭部被指定的),那麼就能夠生成相同的哈希值。
可見,如果伺服器掌握了某個非公開的密鑰,並且將其包含在哈希處理的過程中,那麼就能夠防止黑客自行偽造並生成帶有哈希值的令牌。同時,由於哈希值「掩蓋」了各種原始資訊,因此也就保證了密鑰不會被黑客所發現。
註:將私有數據添加到哈希之中的過程,被稱為加鹽(salting),這使得破解令牌幾乎是不可能的。
身份驗證過程
至此,想必您已經理解了令牌的創建過程。那麼,我們又該如何用它來驗證用戶的API呢?
登錄
在用戶登錄時,系統會生成一個令牌,並將其與用戶模型(model)一起存儲在資料庫中。
logincontrol.js: if (passwordCorrect) { user.token = generateToken(user.id); user.save(); } 然後作為對於登錄請求的響應,該令牌被添加到authorization的頭部。
logincontrol.js:
if (passwordCorrect) { user.token = generateToken(user.id); user.save(); res.headers("authorization",`Bearer ${token}`).send(); } 驗證請求
有了令牌,用戶現在就可以將其添加到各種後續的請求中,以驗明正身了。
而當伺服器收到添加了身份資訊的令牌請求後,會進行如下操作:
- 對令牌進行解碼,並從有效載荷中提取ID。
- 使用此ID,在資料庫中查找該用戶的資訊。
- 將請求令牌與帶有用戶模型的存儲令牌進行比較。如果匹配,則認定該用戶的「合法」身份。
authMiddleware.js:
const token = req.header.token; const payload = decodeToken(token); const user = User.findById(payload.id); if (user.token = token) { // Authorized } else { // Unauthorized } 註銷
如果該用戶要求註銷,那麼系統只需刪除掉當前已添加到用戶模型的令牌便可。由於用戶手上的令牌及時失效了,因此如果他需要再次登錄的話,應重新產生新的令牌。
logoutcontrol.js:
user.token = null; user.save();
總結
通過上面的逐步分析,希望您能夠對於如何使用JSON Web Token來加固API,已經建立起了基本的概念。當然,該話題涵括的內容遠不止這些,如果您有興趣的話,可以通過如下鏈接進行擴展閱讀:
- Jwt.io - https://jwt.io/
- 什麼是JSON Web Token?- https://robmclarty.com/blog/what-is-a-json-web-token

