一、概述
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 後,并不能主動登出,若存在惡意請求則很難制止。其實可以通過 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
覺得寫得不錯可以關注下微信公衆号「松鼠專欄」
