簡介 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 | 使用者請求資料-->request經過load balancer-->到達某一個複本上--> |
小節
透過以上的介紹可以了解,在分散式系統中,基本上難以確保使用者能夠每次都連線到同一個複本服務伺服器。
因此使用者的相關資訊(session)難以留存在複本上,只能存入如Redis…這類作為快取的DB或是透過API傳遞資料給某個紀錄使用者相關資訊的服務主機
這樣在整體的效能上會偏慢,為了驗證身分需要經過好幾道手續。
而cookie更是難以確保資料正確及安全性。
於是乎,最後採取的Token做為網站驗證身分的方式。
基於express搭配JWT的流程
基本上在提供同一個服務內的所有複本伺服器都會使用同一組金鑰,因為採用容器化的方式架構的話。會使用同一個image
所以如果用token在request進來的時候可以直接做運算,只要私鑰沒有外流,基本上都是較為安全的。
1 | request-->load balance-->pod-->某個複本-->伺服器接收到request之後直接根據擁有的私鑰解密驗證request的合法性 |
實務系統架構詳細說明
相比session的流程,減少了對快取或DB的讀寫。同時也不需要綁定網域,這樣一來就可以自己服務之間不同的網域中傳遞資料。
對伺服器的相依減少後,可以從原本的MVC在同一個伺服器中運行,拆解成各個不同的微服務。
使用session/cookie由於對發行的伺服器有依賴,所以不同的middleware間相依性很高。
以nodejs的express為例子。如果採用session或cookie的架構如下
1 | |----------| |
由於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 | |----------| |
這兩個合作起來的流程大概為:
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