淺顯易懂講解如何用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

Tags: