天天看點

JWT 到底應該怎麼用才對?一、概述二、細節三、應用場景四、使用

一、概述

JWT 全稱為 JSON Web Token,是一份開源的标準協定,它定義了一種傳輸内容基于 JSON、輕量級、安全的資料傳輸方式。

二、細節

每個 JWT 都由 Header、Payload、Signature 3 部分組成,同時用點進行拼接,形式如下:

Header.Payload.Signature           

Header

Header 部分是一個經過 Base64 編碼後的 JSON 對象。對象的内容通常包括 2 個字段,形式如下:

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

其中,typ(全稱為 type)指明目前的 Token 類型為 JWT,alg(全稱為 algorithm)指明目前的簽名算法是 HS256。

Payload

Payload 部分也是一個經過 Base64 編碼後的 JSON 對象,對象的屬性可以劃分成 3 部分:保留字段、公共字段、私有字段。

保留字段是 JWT 内部聲明,具有特殊作用的字段,包括

  • iss(全稱為 issuer),指明 JWT 是由誰簽發的
  • sub(全稱為 subject),指明 JWT 的主題(也可了解為面向使用者的類型)
  • aud(全稱為 audience),指明 JWT 希望誰簽收
  • exp(全稱為 expiration time),指明 JWT 的過期時間,過期時間需大于簽發時間
  • nbf(全稱為 not before time),指明 JWT 在哪個時間點生效
  • iat(全稱為 issued at time),指明 JWT 的簽發時間
  • jti(全稱為 JWT ID),指明 JWT 唯一 ID,用于避免重播攻擊

公共字段和私有字段都是使用者可以任意添加的字段,差別在于公共字段是一些約定俗成,被普遍使用的字段,而私有字段更符合實際的應用場景。

目前已有的公共字段可以從

JSON Web Token Claims

中找到。

Payload 的結構形式如下:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}           

Signature

Signature 部分是 JWT 根據已有的字段生成的,它的計算方式是使用 Header 中定義的算法,使用使用者定義的密鑰,對經過 Base64 編碼後的 Header 和 Payload 組成的字元串進行加密,形式如下:

HMACSHA256(base64(header) + '.' + base64(payload))           

三、應用場景

業界普遍認可的應用場景主要有以下幾種:

防止傳輸資料篡改

資料資料篡改指的是資料在傳輸過程中被截獲,修改的行為。

JWT 本身可以使用加密算法對傳輸内容進行簽名,即使資料被截獲,也很難同時篡改簽名和傳輸内容。

鑒權

鑒權指的是驗證使用者是否有通路系統的權利。

部分人使用 JWT 來取代傳統的 Session + Cookie,理由是:

  • 伺服器開銷小。使用 Session + Cookie 需要伺服器緩存使用者資料,而使用 JWT 則是直接将使用者資料下發給用戶端,每次請求附帶一并發送給伺服器。
  • 擴充性好。伺服器不緩存使用者資料的好處是可以很友善的進行橫向擴容
  • 适用于單點登入。JWT 很适合做跨域情況下的單點登入
  • 适用于搭配 RESTFul API 使用。基于 RESTFul 架構設計的 API 需遵循 RESTFul 的無狀态原則,而基于 JWT 的鑒權恰恰是把狀态轉移到了用戶端

基于 JWT 的鑒權一般處理邏輯是:

JWT 到底應該怎麼用才對?一、概述二、細節三、應用場景四、使用

基于 JWT 的鑒權方案也存在一些争議:

  • 伺服器簽發 JWT 後,并不能主動登出,若存在惡意請求則很難制止。其實可以通過 Token 黑名單的方式去解決。
  • JWT 減少了伺服器的開銷,卻增加了帶寬的開銷,JWT 生成的 Token 在體積上比 SessionID 大很多,意味着每次請求相比之前要攜帶更多的資料量。這個确實是這樣,是以應該盡量隻在 JWT 内放必要的資料。
  • JWT 在鑒權方面并非完全優于 Session-Cookie,舉個例子,SessionID 也可以通過簽名的方式來防止篡改。

四、使用

以下使用 Node.js 和 JavaScript 示範 JWT 在鑒權方面的應用,涉及的庫有:

如何生成 Token

Token 的生成一般是用戶端發送登入請求,伺服器使用密鑰生成 Token 并放入響應體中,以下為服務端的 Token 生成邏輯。

// 檔案位置:controller/v1/token.js
const config = require('config') // 加載伺服器配置
const jwt = require('jsonwebtoken') // 加載 jwt Node.js 語言實作

/**
 * 建立 Token 控制器
 * @param {Object} ctx 請求上下文
 */
async function create(ctx) {
  const username = ctx.request.body.username
  const password = ctx.request.body.password
  
  if (!username || !password) {
    ctx.throw(400, '參數錯誤')
    return
  }
  
  // 省略:使用者名密碼資料庫校驗
  const user = { id: '5e54c02a2b073de564fe8034' } // 使用者資訊
  const secret = config.get('secret') // 擷取儲存于配置中的密鑰
  const opt = { expiresIn: '2d' } // 設定 Token 過期時間為 2 天
  
  ctx.body = jwt.sign(user, secret, opt) // 生成并傳回 token
}

module.exports = {
  create,
}           

用戶端攜帶 Token 進行請求

用戶端一般情況下将 Token 放在 Http Header 的 Authorization 中,随請求發送給伺服器。

// 檔案位置:views/index.pug
var request = axios.create({ baseURL: '/api/v1' }) // 建立請求執行個體
var token // 為了友善這裡使用全局變量,正常情況下應該放入其他存儲媒體中,如,localStorage,此處省略擷取邏輯

// 監聽正常請求按鈕單擊事件,發起請求
document.querySelector('#normal').addEventListener('click', function() {
  if (!token) {
    alert('請登入')
    return
  }
  
  request.get('/users', {
    headers: {
      Authorization: 'Bearer ' + token, // 綁定 token 到 header 中
    },
  }).then(function({ data }) {
    document.querySelector('#response').innerHTML = JSON.stringify(data)
  }).catch(function(err) {
    console.log('Request Error: ', err)
  })
})           

伺服器如何驗證 Token

驗證操作一般放在伺服器的中間件。

const config = require('config') // 加載伺服器配置
const jwt = require('jsonwebtoken') // 加載 jwt Node.js 語言實作

// 定義中間件函數
module.exports = async (ctx, next) => {
  const path = ctx.url // 擷取請求 URL
  const method = ctx.method.toLowerCase() // 擷取請求方法
  
  // 請求白名單,白名單中的請求不經過中間件 token 校驗
  const whiteList = [
    { path: /^\/api\/v[1-9]\/tokens/, method: 'post' },
    { path: /^\/api/, reverse: true }, // 非 /api 開頭的資源都不需要經過請求校驗
  ]
  
  // 請求白名單檢查函數  
  const checker = (i) => {
    const matchPath = i.path.test(path)
    const matchMethod = i.method ? i.method === method : true
    return (i.reverse ? !matchPath : matchPath) && matchMethod
  }
  
  // 白名單邏輯判斷
  if (whiteList.some(checker)) {
    await next()
    return
  }
  
  // 擷取 http header 中的 token
  const token = (ctx.header.authorization || '').replace('Bearer ', '')
  
  // token 有效性校驗
  try {
    const data = jwt.verify(token, config.secret)
    ctx.userInfo = data
  } catch (e) {
    ctx.throw(400, 'Token 錯誤')
  }
  
  await next()
}           

檢視完整代碼請前往 GitHub 搜尋使用者 yo-squirrel

覺得寫得不錯可以關注下微信公衆号「松鼠專欄」

JWT 到底應該怎麼用才對?一、概述二、細節三、應用場景四、使用

繼續閱讀