JWT淺析

一、概念

Json web token (JWT)是啥?

JWT 是 JSON Web Token 的縮寫,JWT 本身沒有定義任何技術實現,它只是定義了一種基於 Token 的會話管理的規則,涵蓋 Token 需要包含的標準內容和 Token 的生成過程。

是爲了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準((RFC 7519).該token被設計爲緊湊且安全的,特別適用於分佈式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。

二、背景分析

HTTP 是一個無狀態的協議,一次請求結束後,下次在發送服務器就不知道這個請求是誰發來的了(同一個 IP 不代表同一個用戶),在 Web 應用中,用戶的認證和鑑權是非常重要的一環,實踐中有多種可用方案,並且各有千秋。

1.傳統基於session的會話管理/認證

a. session基本認知

在 Web 應用發展的初期,大部分採用基於 Session 的會話管理方式,邏輯如下:

  • 客戶端使用用戶名密碼進行認證

  • 服務端認證成功後,生成並存儲 Session,將 SessionID 通過 Cookie 返回給客戶端

  • 客戶端訪問需要認證的接口時在 Cookie 中攜帶 SessionID,服務端通過 SessionID 查找 Session 並進行鑑權,返回給客戶端需要的數據在這裏插入圖片描述

b. session的認證方式暴露的問題

  • 服務端開銷大:爲便用戶下次請求的鑑別,服務端會存儲session,爲了快速認證,通常而言session都是保存在內存中,而隨着認證用戶的增多,服務端的開銷會明顯增大。

  • 擴展差:session存儲在客戶當時請求的服務器的內存中,當需要做分佈式等擴展性服務時,數據同步的問題,會變得很麻煩,相應的限制了負載均衡器的能力。這也意味着限制了應用的擴展能力。

  • 易被CSRF攻擊:由於客戶端使用 Cookie 存儲 SessionID,在跨域場景下需要進行兼容性處理,同時這種方式也難以防範 CSRF 攻擊。

2. 基於token的會話管理/認證

鑑於基於 Session 的會話管理方式存在上述多個缺點,無狀態的基於 Token 的會話管理方式誕生了,所謂無狀態,就是服務端不再存儲信息,甚至是不再存儲 Session,意味着基於token認證機制的應用不需要去考慮用戶在哪一臺服務器登錄了,這就爲應用的擴展提供了便利,邏輯如下:

  • 客戶端使用用戶名密碼進行認證
  • 服務端認證成功後,利用特定算法,生成token,將token返回給客戶端
  • 客戶端訪問需要認證的接口時在 URL 參數或 HTTP Header (推薦)中加入 Token,服務端通過解碼 Token 進行鑑權,返回給客戶端需要的數據
    在這裏插入圖片描述

基於 Token 的會話管理方式有效解決了基於 Session 的會話管理方式帶來的問題。

  • 服務端不需要存儲和用戶鑑權有關的信息,鑑權信息會被加密到 Token 中
  • 服務端只需要讀取 Token 中包含的鑑權信息即可
  • 避免了共享 Session 導致的不易擴展問題不需要依賴 Cookie,有效避免 Cookie 帶來的 CSRF 攻擊問題
  • 服務器使用 CORS(跨域資源共享) 可以快速解決跨域問題Access-Control-Allow-Origin: *

三、主角JWT

1. jwt基本結構

Jwt是由三段信息構成的,將這三段信息文本用.鏈接一起就構成了Jwt字符串。就像這樣

jwt 結構生成下

  • 其構成由三部分構成,分別爲:頭部(header),載荷(payload),簽證(signature).
  • 頭部和負載以json形式存在,三部分分別單獨經過了base64編碼,並以"."拼接成一個JWT Token

A .頭部 header

頭部承載兩部分信息:

  • 聲明類型 typ

  • 聲明加密算法alg,通常是使用默認的 HMAC SHA256

// 完整頭部header格式如下
{
	"typ": "JWT",
	"alg": "HS256"
}
//經過base64(urlsafe_b64encode,並把補位符"="換成了"",下同)編碼轉換,得到jwt第一部分 header_b64
// python 實現:header_b64 = base64.urlsafe_b64encode(json.dumps(header, separators=(',', ':')).encode("utf-8")).replace(b'=', b'')

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

B. 負載 payload

負載(載荷)是存放有效信息的地方,通常會存放一些跟用戶相關的自定義信息以及過期時間之類的一些必要信息。

jwt規範中規定了一些字段,並推薦使用。當然開發者也可自定義。

負載也可以認爲有三個部分組成(其實就是不同約定的字段):標準中註冊的聲明、公共的聲明、私有的聲明

  • 標準中註冊的聲明(建議但不強制使用,一種約定而已)

    • iss: jwt簽發者
  • sub: jwt所面向的用戶

    • aud: 接收jwt的一方
  • iat: jwt的簽發時間(unix時間戳)

    • exp: jwt的過期時間,這個過期時間必須要大於簽發時間(unix時間戳)
  • nbf: 定義在什麼時間之前,該jwt都是不可用的.

    • jti: jwt的唯一身份標識,主要用來作爲一次性token,從而回避重放攻擊。
  • 公共的聲明

    • 公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息
  • 私有的聲明

    • 私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息
// payload實例
{
  "sub": "7ec1047d81bbc53cce3aa93483551e1c",
  "name": "Lucy",
  "role": "admin",
  "exp": 1567652129
}
// 經過base64(urlsafe_b64encode)編碼轉換,得到jwt第二部分 payload_b64
// python實現:payload_b64 = base64.urlsafe_b64encode(json.dumps(payload, separators=(',', ':')).encode("utf-8")).replace(b'=', b'')

eyJzdWIiOiI3ZWMxMDQ3ZDgxYmJjNTNjY2UzYWE5MzQ4MzU1MWUxYyIsIm5hbWUiOiJMdWN5Iiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNTY3NjUyMTI5fQ

注意: payload的內容只不過是經過了base64編碼,相當於明文存儲,所以切忌防止敏感信息!

C. 簽證/簽名 signature

  • 對頭部和負載信息,做簽名認證,防止被篡改。

  • 需要三部分來生成簽名

    • header(經過base64編碼後的頭部)
    • payload(經過base64編碼後的負載)
    • secret(進行加密需要的祕鑰)
# 實現方式(python)
# 將 頭部header_b64 和 負載payload_b64 用"."連接起來,指定加密的key
import base64
import json
import hmac

secret_key = "abcd123456"
a = hmac.new(secret_key.encode("utf-8"), b'.'.join([header_b64, payload_b64]), digestmod="sha256").digest()
signature = base64.urlsafe_b64encode(a).replace(b'=', b'')
segments.append(signature)

# 得到第三部分簽名值
rxD-kI42kRIFHNSMcjoJe30sbcHToyP_VsWG6kH_uMI
// js實現
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, secret_key);

將三部分用"."連起來就是一個完整的token

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI3ZWMxMDQ3ZDgxYmJjNTNjY2UzYWE5MzQ4MzU1MWUxYyIsIm5hbWUiOiJMdWN5Iiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNTY3NjUyMTI5fQ.rxD-kI42kRIFHNSMcjoJe30sbcHToyP_VsWG6kH_uMI

注意:

secret_key是需要嚴格保密的,如果這部分泄漏出去了,客戶端就可以隨意簽發token憑證了!

# python中jwt包的適用
# 結合上述的三部分數據
import jwt
token = jwt.encode(
    payload=payload,
    key=secret_key,
    algorithm='HS256'
)
print(token)
# 得到的結果,跟上述自己加密是一摸一樣的
# 自實現的加密中,把補位符"="換成了"",是由於jwt包內部是這麼實現的,爲了保證兩中方式得到的值一樣,所以也做了同樣的操作。

2.使用

  • 一般是約定好字段,加在請求頭中,傳遞給服務器,由其解析。如:TOKEN
  • 服務端會驗證token,如果驗證通過就會返回相應的資源

大致流程如下:
在這裏插入圖片描述

3. jwt優缺點分析

  • JWT 擁有基於 Token 的會話管理方式所擁有的一切優勢,不依賴 Cookie,使得其可以防止 CSRF 攻擊,也能在禁用 Cookie 的瀏覽器環境中正常運行。

  • 服務端不再需要存儲 Session,使得服務端認證鑑權業務可以方便擴展,避免存儲 Session 所需要引入的 Redis 等組件,降低了系統架構複雜度。

  • 因爲json的通用性,所以JWT是可以進行跨語言支持

  • jwt的構成非常簡單,字節佔用很小,便於傳輸

  • 服務端不存儲token,也是 JWT 最大的劣勢,由於有效期存儲在 Token 中,JWT Token 一旦簽發,就會在有效期內一直可用,無法在服務端廢止,當用戶進行登出操作,只能依賴客戶端刪除掉本地存儲的 JWT Token,如果需要禁用用戶,單純使用 JWT 就無法做到了

  • 一定要保護好secret私鑰,該私鑰非常重要!

4. jwt的引申擴展

針對jwt的簽發後,不易控制的缺點,我們可以做些折中方案,弱化它的影響。

引入 Refresh Token(token續簽模式)

  • 客戶端使用用戶名密碼進行認證服務端生成有效時間較短Access Token(例如 10 分鐘),和有效時間較長的 Refresh Token(例如 7 天)客戶端訪問需要認證的接口時,攜帶 Access Token如果 Access Token 沒有過期,服務端鑑權後返回給客戶端需要的數據如果攜帶 Access Token 訪問需要認證的接口時鑑權失敗(例如返回 401 錯誤),則客戶端使用 Refresh Token 向刷新接口申請新的 Access Token如果 Refresh Token 沒有過期,服務端向客戶端下發新的 Access Token客戶端使用新的 Access Token 訪問需要認證的接口
  • Refresh Token是存儲在服務器中,如果它過期了,那麼就無法換取token,得以有效的控制
  • Refresh Token只是用來換token,請求量遠低於token驗證,可保存在sql數據庫中,對於性能影響不會很大
  • 結合客戶端的主動退出登錄,也可以刪除所有tokenRefresh Token,使得控制粒度變得更細

流程:
在這裏插入圖片描述


5. 思考問題:

  • 如果token沒過期,用RefreshToken來刷新token的話 ,因爲JWT是無狀態的,那麼就會存在多個有效Token,怎麼處理?
  • 如果要維護黑名單人員,那麼是不是又回到了,存儲token的方式,設計token數據同步?
  • … … …

以上相關以及擴展問題,這裏有很詳細的分析,寫的很不錯!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章