0%

JSON web Token 及分散式系統應用簡介

簡介 Intro

什麼是JWT? 今天想記錄一下不同於以往的cookie以及session,在後端驗證身分的新方式 “Token”。

在分散式系統中,驗證身分是一件複雜的事情。特別是如果要設計一套具備高可靠、高效能、高彈性的分散式系統架構

驗證身分相較於在單機伺服器上會變成一個更加複雜的事情


JSON Web Token(JWT)

JWT全名為JSON Web Token,是一種基於JSON的開放標準RFC 7519,他定義了一種簡潔(compact)且自包含(self-contained)的方式。

用於在雙方之間安全地將訊息作為JSON物件傳輸。而且這個訊息是經過數位簽章(Digital Signature)。

因此可以被驗證及信任,可以使用密碼或是用一對公鑰/私鑰來對JWT進行簽章

註:

  • 簡潔(compact):體積非常的小,可放在 URL 、 POST body 或 HTTP Header 內發送請求,體積小意味著傳輸速度快。
  • 自包含(self-contained):payload 裡面就有所需要的資訊,不需要再重新 query database 的資料。
  • 數位簽章

哪種情況適合使用JWT

  • 授權(Authorization):這是很常見 JWT 的使用方式,例如使用者從 Client 端登入後,該使用者再次對 Server 端發送請求的時候,會夾帶著 JWT,允許使用者存取該 token 有權限的資源。單一登錄(Single Sign On)是當今廣泛使用 JWT 的功能之一,因為它的成本較小並且可以在不同的網域(domain)中輕鬆使用。

  • **訊息交換(Information Exchange)**:JWT 可以透過公鑰/私鑰來做簽章,讓我們可以知道是誰發送這個 JWT,此外,由於簽章是使用 header 和 payload 計算的,因此還可以驗證內容是否遭到篡改。


JWT的組成結構

每個JWT都包含三部分訊息,以.作為區隔分開,第一部分為Header,第二部分為Payload,前面兩個部分為特定結構的JSON。

第三部分取決於演算法用於簽章或是加密,如果是未加密的JWT,則將其省略。

JWT可以被編碼為JWS(JSON Web Signatures)JWE(JSON Web Encryption) 簡潔的表現形式(Compact Serialization)。

在JWS和JWE規範中定義了另一種序列化格式,稱為JSON序列化,這是一種非簡潔的表示形式。

允許在同一個JWT中使用多個簽章或接收者

簡潔的序列化(The compact serialization)是對前兩個UTF-8字節的JSON元素(Header和Payload)

以及簽章或加密的data(不是JSON物件本身)做Base64 URL編碼。

  • Header

    每個JWT都有一個Header(又稱JOSE Header),是一種對自我的聲明

    無論JWT是簽章或加密,這些聲明都表示了其所使用的演算法,通常也會表示要如何解析JWT的其餘部分。

    根據JWT類型,Header中可能必須包含更多字段。加密的JWT攜帶有關用於密鑰加密和內容加密的加密演算法的訊息。未加密的JWT,這些字段則不存在

    Header包含

    • 必要欄位

      • algorithm: 對此JWT進行簽章、加密或解密的主要演算法。對於未加密的JWT,此欄位必須設置為none
    • 非必要欄位

      • typ: JWT本身的媒體類型。此參數有助於將JWT與帶有JOSE header的其他Object混合使用的情況。

        但實際上這種事情很少發生。如果這種情況存在,則此參數應設置為”JWT”

      • cty: 內容類型。(Content Type)

        大多數JWT攜帶的特定聲明以及任意數據,作為其Payload的一部分,在這種情況下,不得設置內容參數。

        對於Payload本身是JWT自己(巢狀JWT)的Instance,此參數必須存在,並設定其值為”JWT”。

        用來表示需要進一步處理巢狀的JWT。而巢狀的JWT很少見,因此cty很少出現在header中

  • Payload

    通常與使用者有關的資訊都會被放在Payload之中,Payload是一個JSON物件。

    JWT規範指出,應該忽略在實踐中無法理解的聲明,具有所附特定含意的權利要求稱為Registered claims

    Registered Claims 包含

    • iss: issuer的簡稱,用字串(case-sensitive)或URI表示這個JWT的唯一識別的發行方
    • sub: subject的簡稱,用字串(case-sensitive)或URI表示這個JWT所夾帶的唯一辨別方式

    此JWT中包含的聲明,是關於Object的聲明,JWT規範規定,此聲明在發行方的上下文中必須是唯一的,或者在不可能的情況下必須是全局唯一。

    • aud: audience的簡稱,用字串(case-sensitive)或URI或陣列表示這個JWT唯一識別的預期接收者。

      也就是說,當此參數存在時,則讀取此JWT數據的一方必須在aud中找到自己,或者無視JWT中包含的數據。

    • exp: expiration(time)的簡稱,一個用來表示特定日期或時間的數字,格式為POSIX定義的自紀元以來的秒數,也就是UNIX時間

      此聲明設置了該JWT被視為無效的確切時間,實務上可能允許時間存在一些偏差(考慮此JWT到期日後的幾分鐘內有效)

    • nbf: not before (time) 的簡稱,exp 的相反,格式同 exp,當前時間和日期必須等於或晚於該日期和時間。一些實踐可能允許一定的偏差。

    • iat: issued at (time) 的簡稱,一個用來表示特定日期和時間的數字(格式同 exp 和 nbf),即該 JWT 發行的時間。

    • jti: JWT ID 的簡稱,一個字串表示這個唯一識別的 JWT。此聲明可用於區分具有其他相似內容的 JWT (例如,防止重放)。取決於實現以確保唯一性。

    所有的聲明,只要不在 registered claims 裡的,不是 private claims 就是 public claims。

    Private Claims:

    是由 JWT 的使用者(消費者和生產者)定義的那些。換句話說,這些是用於特定情況的臨時聲明。因此,必須注意防止衝突(collisions)。

    Public Claims:

    IANA JSON Web Token 聲明註冊表上註冊的聲明(用戶可以註冊其聲明,以防止衝突),或者是使用抗衝突名稱命名的聲明(例如,在名稱前添加 namespace)。

  • Signature/Encryption Data:

    用於存放簽署或加密後的資料

小節

在看過JWT的簡易介紹後相信對於JWT的特性有一定的了解,如果想看更加詳細的資料歡迎參閱這裡


實際應用

介紹

這個情況用於一個處理IoT的分散式系統,在接收來自IoT設備的數據,處理過後在網站上呈現視覺資料。
並且可以透過網站,對IoT設備進行遠程操作。

設計網站的第一步,肯定是需要處理使用者的登入、註冊以及身分驗證等業務邏輯。

分散式系統以及微服務特性

在分散式系統架構中,session在微服務架構中是一個不太可靠且開發成本大的身分驗證方式,由於session需要在伺服器儲存相關資料,所以流程通常如下

基於express搭配session/cookie的流程

1
2
3
4
5
6
7
8
9
使用者請求資料-->request經過load balancer-->到達某一個複本上-->

-->複本伺服器接收到使用者的請求-->解析使用者的request-->搜尋DB或cache找到相關的驗證資訊-->比對驗證資料

如果驗證成功:
根據使用者的request進行下一步操作

如果驗證失敗:
回傳失敗相關訊息

小節

透過以上的介紹可以了解,在分散式系統中,基本上難以確保使用者能夠每次都連線到同一個複本服務伺服器。

因此使用者的相關資訊(session)難以留存在複本上,只能存入如Redis…這類作為快取的DB或是透過API傳遞資料給某個紀錄使用者相關資訊的服務主機

這樣在整體的效能上會偏慢,為了驗證身分需要經過好幾道手續。

而cookie更是難以確保資料正確及安全性。

於是乎,最後採取的Token做為網站驗證身分的方式。

基於express搭配JWT的流程

基本上在提供同一個服務內的所有複本伺服器都會使用同一組金鑰,因為採用容器化的方式架構的話。會使用同一個image

所以如果用token在request進來的時候可以直接做運算,只要私鑰沒有外流,基本上都是較為安全的。

1
2
3
4
request-->load balance-->pod-->某個複本-->伺服器接收到request之後直接根據擁有的私鑰解密驗證request的合法性

如果合法:進行下一步操作
如果不合法(過期/遭到修改無法解密):回傳request不合法的資訊

實務系統架構詳細說明

相比session的流程,減少了對快取或DB的讀寫。同時也不需要綁定網域,這樣一來就可以自己服務之間不同的網域中傳遞資料。

對伺服器的相依減少後,可以從原本的MVC在同一個伺服器中運行,拆解成各個不同的微服務。

使用session/cookie由於對發行的伺服器有依賴,所以不同的middleware間相依性很高。

以nodejs的express為例子。如果採用session或cookie的架構如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|----------|
| P |
| O |
| D |=========>假設這裡面包含了30個nodejs伺服器,都使用同樣的程式碼
| |
| A |
|----------|
-------------------------------------------------------------------------
那其中一個Node.js伺服器架構長相大致上

|
|
|----bin(server)
|
|----route(middleware)
| |
| |------index.js
| |
| |------author.js(查詢DB驗證資料)
| |
| |------dosomeing.js
| |
| |------parse.js
|
|----view(viewer)
|
|----app.js(control)

由於session/cookie對發行的環境依賴

當有request進入伺服器後,伺服器必須先進行一次撈取資料的動作,驗證完畢後驗證資料只留存在這個伺服器中。

因此其他29個伺服器,並不知道這個request是合法的。

為了避免反覆的驗證以及資料傳遞上的複雜,驗證完畢後的request隨後需要在這個伺服器上解析請求,並且完成他

author.js–>index.js—>parse.js—>dosomeing.js

實際上流程應該是

author.js–>queryDB–>author.js—>index.js—>parse.js—>dosomeing.js

這樣會導致同一個伺服器中負擔的責任太多,有可能產生賭塞。
最大的問題是,效率運用不佳以及權責沒有單一化(可以簡單理解為 良好設計上函式或物件只做單一的事情,那微服務的概念也差不多)

這個問題當然也有辦法解決,但是相對於token來說需要花費更多的時間以及心力。那何不直接使用token呢?

以nodejs的express為例子。如果採用token的架構如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|----------|
| P |
| O |
| D |=========>假設這裡面每個POD包含了30個nodejs伺服器,都使用同樣的程式碼
| |
| A/B |
|----------|
-------------------------------------------------------------------------
那POD A其中一個Node.js伺服器架構長相大致上

|
|
|----bin(server)
|
|----route(middleware)
| |
| |------index.js
| |
| |------author.js(驗證資料)
| |
| |------pass.js(傳遞資料給下一個微服務)
| |
| |------reject.js(拒絕request)
|
|----view(viewer)
|
|----app.js(control)

-------------------------------------------------------------------------
那POD B其中一個Node.js伺服器架構大致上

|
|
|----bin(server)
|
|----route(middleware)
| |
| |------index.js
| |
| |------dosomething.js
| |
| |------pass.js
| |
| |------reject.js
|
|----view(viewer)
|
|----app.js(control)

-------------------------------------------------------------------------

這兩個合作起來的流程大概為:

request–>POD A–>author.js–>index.js–>pass.js | reject.js

POD A處理完之後使用pass.js將資料傳遞給POD B

request–>POD B–>dosomething.js–>index.js—>pass.js | reject.js

透過以上流程,可以看到。request在傳入單個複本時,不必再消耗效能對DB操作。

同時由於對簽署的伺服器沒有依賴性,可以很輕易的的請求傳遞給其他服務進行操作。

解決了session/cookie在分散式系統中難以應用的問題。


結論

由於session/cookie對發行的環境依賴

當有request進入伺服器後,伺服器必須先進行一次撈取資料的動作,驗證完畢後驗證資料只留存在這個伺服器中。

因此其他29個伺服器,並不知道這個request是合法的。

為了避免反覆的驗證以及資料傳遞上的複雜,驗證完畢後的request隨後需要在這個伺服器上解析請求,並且完成這個請求動作。

這個問題雖然能夠透過快取等或其他設計方式解決,但相比Token設計需要花費更大的心力處理。

使用Token既不需要考慮跨網域跨單元的問題,在資料傳遞上更加單純。安全性方面來說,相較session/cookie的模式也絲毫不遜色

因此如果看到這篇文章的讀者,正在設計分散式系統驗證身分的部份的話,可以多加思考驗證身分需要花費的運算資源。

如果是需要做單一登入的功能,那更恭喜你。你找到了一個很好的解決方案,Token絕對是可以納入考量之一的實踐方式

微服務的設計可以使每個POD的職責更加單純,在資源的運用上更加有效率。使用Token傳遞資料,除了驗證身分單純外,也減少了資源的浪費


註解

  • 英文或真正的學術名詞,還請不吝糾正

  • POD其實是筆者借鑑k8s的名詞,但概念上基本相同。筆者目前第一版IoT系統架構實作方式,在GCP上實作

    由附載平衡器將大量請求平均到不同的Instance上,每個Instance包含了Nginx/Docker/Custom Manager

    先將請求透過NGX分類平衡到不同的容器中,再透過Manager傳遞容器間的資料,或是分配任務

    這個概念由於不太會稱呼,在後來學習K8S了解到與筆者理念相同的設計,因此筆者只針對自己的文章暫時稱這樣的模式為POD

  • 如果有任何指教歡迎與我聯繫,我會立刻改善。或是有任何問題想提出,我有空的時候會回答

  • 聯絡方式github teddy1565

參考資料