一篇文章告訴你JWT的實現原理

編輯:業餘草 來源:https://www.xttblog.com/?p=4940

寫篇文章不容易,昨天晚上深夜寫完忘記保存了。聯想一下那個畫面,真的是淚目啊!

哎,過去的就讓他過去吧,今天我們繼續說 JWT。

在使用 JWT 的時候,有沒有想過,為什麼我們需要 JWT?以及它的工作原理是什麼?

我們就來對比,傳統的 session 和 JWT 的區別

我們以一個用戶,獲取用戶資料的例子

傳統的 session 流程

  1. 瀏覽器發起請求登陸
  2. 服務端驗證身份,生成身份驗證資訊,存儲在服務端,並且告訴瀏覽器寫入 Cookie
  3. 瀏覽器發起請求獲取用戶資料,此時 Cookie 內容也跟隨這發送到伺服器
  4. 伺服器發現 Cookie 中有身份資訊,驗明正身
  5. 伺服器返回該用戶的用戶資料

JWT 流程

  1. 瀏覽器發起請求登陸
  2. 服務端驗證身份,根據演算法,將用戶標識符打包生成 token, 並且返回給瀏覽器
  3. 瀏覽器發起請求獲取用戶資料,把剛剛拿到的 token 一起發送給伺服器
  4. 伺服器發現數據中有 token,驗明正身
  5. 伺服器返回該用戶的用戶資料

你發現了嗎?好些並沒有什麼區別,除了 session 需要服務端存儲一份,而 JWT 不需要

但實際上區別大了去了

  1. session 存儲在服務端佔用伺服器資源,而 JWT 存儲在客戶端
  2. session 存儲在 Cookie 中,存在偽造跨站請求偽造攻擊的風險
  3. session 只存在一台伺服器上,那麼下次請求就必須請求這台伺服器,不利於分散式應用
  4. 存儲在客戶端的 JWT 比存儲在服務端的 session 更具有擴展性

對比完了 session 和 JWT 的區別,下面我們來看看那它的實現原理

JWT 工作原理

首先 JWT 長這個樣

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpcCI6IjEyNy4wLjAuMSIsInV1aWQiOiJmZjEyMTJmNS1kOGQxLTQ0OTYtYmY0MS1kMmRkYTczZGUxOWEiLCJpYXQiOjE1Mjc1MjMwMTd9.1C01cpOf1N3M9YAYvQjfcLbDGHZ4iPVncCGIoG-lpO0jHOIAZHtSMDvK1nzArLpGK5syQSwExsZJz2FJsd2W2TUiHQYtzmQTU8OBXX6mfSZRlkts675W5WhIiOEwz69GFSD0AKXZifCRgIpKLC0n273MRMr0wJnuBi9ScfJ7YjSiqCr7qyQ5iXeOdS3ObT3wdjjk-Wu9wbWM7R25TFb-7PEZY7re8jmczPCVcNbOYegedu73T4d30kRn2jKufTGntD5hR6T9AQsgAMwVR1edEFflWb772TmrHI7WZOAivsBCN9sr4YiyYMvE8lczmBsgsunugGiHA3DGxB2ORbjIC8NPm8FI25zGOh9JIM2rjFFTIm9GiuKtC8Ck8N3-eWi9u1NgBxwLdgN5JyCORnIOlciQEsScg-3SdCTM5LHj6CeqQNwJxT4-oENzqLSTDJbP-SOj9nnx8HnJ5wh3n64rAvtc89CeTk7PhWFjksHDifngN-cnaszl5lqoF1enz5i9FYYELSjM-G7jns2SyY1MQeLRjuEDriPZtFaGmTW-RLH3gJfQXtbdpEo0nHBqXEohwoNFLKo4BNrEwshpyA7vkBpCQC0QALKyC1_L1Q5qduR6dDcqRozAo2VqJXmAKN0rvhLuIEHZkicOZY0Ds4So4GCcleqvFcxm1HQ

眼睛看仔細一些,你會發現 JWT 裡面有兩個.

數據格式是這樣的 header.payload.signature

我們逐個逐個部分去分析,這個部分到底是幹嘛的,有什麼用

JWT 的 header 中承載了兩部分資訊

{    "alg": "RS256",    "typ": "JWT"  }
  • alg: 聲明加密的演算法
  • typ: 聲明類型

對這個頭部資訊進行 base64,即可得到 header 部分

const headerBuff = Buffer.from(    JSON.stringify({      alg: "RS256",      typ: "JWT"    })  );  const header = headerBuff.toString("base64");  console.log(header);  // eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9

Payload

payload 是主體部分,意為載體,承載著有效的 JWT 數據包,它包含三個部分

  • 標準聲明
  • 公共聲明
  • 私有聲明

標準聲明的欄位

interface Stantar {    iss?: string; // JWT的簽發者    sub?: string; // JWT所面向的用戶    aud?: string; // 接收JWT的一方    exp?: number; // JWT的過期時間    nbf?: number; // 在xxx日期之間,該JWT都是可用的    iat?: number; // 該JWT簽發的時間    jti?: number; //JWT的唯一身份標識  }

標準中建議使用這些欄位,但不強制。

公共聲明的欄位

interface Public {    [key: string]: any;  }

公共聲明欄位可以添加任意資訊,但是因為可以被解密出來,所以不要存放敏感資訊。

私有聲明的欄位

interface Private {    [key: string]: any;  }

私有聲明是 JWT 提供者添加的欄位,一樣可以被解密,所以也不能存放敏感資訊。

上面的 JWT 的 payload 結構是這樣的

{    "ip": "127.0.0.1",    "uuid": "ff1212f5-d8d1-4496-bf41-d2dda73de19a",    "iat": 1527523017  }

同樣是通過 base64 加密生成第二部分的 payload

const payloadBuffer = Buffer.from(    JSON.stringify({      ip: "127.0.0.1",      uuid: "ff1212f5-d8d1-4496-bf41-d2dda73de19a",      iat: 1527523017    })  );  const payload = payloadBuffer.toString("base64");  console.log(payload);  // eyJpcCI6IjEyNy4wLjAuMSIsInV1aWQiOiJmZjEyMTJmNS1kOGQxLTQ0OTYtYmY0MS1kMmRkYTczZGUxOWEiLCJpYXQiOjE1Mjc1MjMwMTd9

Signature

signature 是簽證資訊,該簽證資訊是通過header和payload,加上secret,通過演算法加密生成。

公式 signature = 加密演算法(header + "." + payload, 密鑰);

上面的 header 中,我們已經定義了加密演算法使用 RS256,也已經實現了生成header和payload,下面我們來生成 signature

const crypto = require("crypto");  const sign = crypto.createSign("SHA256");  const secret = `私鑰,太長我就不貼出來了`;  sign.write(header + "." + payload);  sign.end();  const signature = sign    .sign(secret, "base64")    // 在JWT庫中,已經把這些字元過濾掉了    .replace(/=/g, "")    .replace(/+/g, "-")    .replace(///g, "_");  console.log(signature);

到此,已經實現了如何生成一個 JWT 的 token.

它是如何做身份驗證的?

首先,JWT 的 Token 相當是明文,是可以解密的,任何存在 payload 的東西,都沒有秘密可言,所以隱私數據不能簽發 token。

而服務端,拿到 token 後解密,即可知道用戶資訊,例如本例中的uuid

有了 uuid,那麼你就知道這個用戶是誰,是否有許可權進行下一步的操作。

Token 的過期時間怎麼確定?

payload 中有個標準欄位 exp,明確表示了這個 token 的過期時間.

服務端可以拿這個時間與伺服器時間作對比,過期則拒絕訪問。

如何防止 Token 被串改?

此時 signature欄位就是關鍵了,能被解密出明文的,只有header和payload

假如黑客/中間人串改了payload,那麼伺服器可以通過signature去驗證是否被篡改過。

在服務端在執行一次 signature = 加密演算法(header + "." + payload, 密鑰);, 然後對比 signature 是否一致,如果一致則說明沒有被篡改。

所以為什麼說伺服器的密鑰不能被泄漏。

如果泄漏,將存在以下風險:

  • 客戶端可以自行簽發 token
  • 黑客/中間人可以肆意篡改 token

安全性相關

如果加強 JWT 的安全性?

根據我的使用,總結以下幾點:

  1. 縮短 token 有效時間
  2. 使用安全係數高的加密演算法
  3. token 不要放在 Cookie 中,有 CSRF 風險
  4. 使用 HTTPS 加密協議
  5. 對標準欄位 iss、sub、aud、nbf、exp 進行校驗
  6. 使用成熟的開源庫,不要手賤造輪子
  7. 特殊場景下可以把用戶的 UA、IP 放進 payload 進行校驗(不推薦)

參考文獻

  • RFC 7519

以上純屬個人愚見,大家笑笑就好. 本來 JWT 已經是爛大街的東西,奈何還是有些所謂的「架構師」在不了解的情況下,誤用,誤導廣大群眾!