想全面理解JWT?一文足矣!

技術人成長發表於2019-06-22

有篇關於JWT的文章,叫“JWT: The Complete Guide to JSON Web Tokens”,寫得全面細緻。為了自己能更清晰理解並惠及更多人,我把它大致翻譯了過來,有些地方稍顯冗餘就去掉了,但還是接近八千字,感謝原作者!以下是正文:

本文的目標是讓你學習JWT的工作原理和細節,以及它在Web應用中能如何幫助你實現使用者認證和會話管理功能。那為什麼需要深入理解JWT呢?因為這樣有助於你:

  1. 實現一個基於JWT的認證方案;
  2. 各種故障排查:理解錯誤資訊、堆疊資訊;
  3. 選擇第三方庫,並理解他們的文件;
  4. 自己實現認證方案;
  5. 選擇和配置第三方認證服務;

即使選擇了某個基於JWT的認證方案,同樣還需要進行程式碼編寫,編碼工作主要是在客戶端,但服務端也需要一些。

到本文的結尾,你將深刻理解JWT,包括它基於的加密技術,這種加密技術也廣泛使用在其他安全案例中。你還將明白什麼時候該用JWT和為什麼要使用,同時理解JWT的資料格式,可以使用各種線上工具去分析解決簽名上遇到的問題。

為什麼是JWT?

相比在記憶體中儲存隨機token的使用者會話管理方式來說,JWT最大的優勢是,它使得將認證邏輯委託給第三方服務成為可能,這些服務包括:

  1. 自己開發的、中心化的認證伺服器;
  2. 能生成JWT的LDAP服務;
  3. 完全是外部的第三方認證服務提供商,比如Auth0;

外部認證服務可以完全獨立於我們自己的應用服務,並且不需要通過網路共享任何金鑰資訊。應用伺服器不需要安裝任何金鑰,減少了金鑰丟失或者被盜竊的風險。

此外,應用服務可以完全無狀態,因為不需要在多個請求之間將token儲存在記憶體。認證服務可以在生成token並返回給客戶端後,馬上丟棄它!同樣,密碼摘要也沒有必要儲存在應用資料庫中,因此減少了被盜的風險和其它安全相關的bug。

此刻也許你心裡想:我有一個內部應用,對此,JWT是一個好的方案嗎?是的,在本文的最後一節裡,我們將討論JWT在這種典型場景下的使用情況。

  • 目錄

本文我們將討論以下這些話題:

  1. 什麼是JWT?
  2. JWT線上驗證工具;
  3. JWT的格式;
  4. JWT的核心要素: Header, Payload, Signature;
  5. Base64Url (vs Base64);
  6. 使用JWT進行會話管理: 主題和期限
  7. HS256簽名 – 它是如何工作的?
  8. 數字簽名;
  9. 雜湊函式和SHA-256;
  10. RS256 JWT簽名 – 談談公鑰加密;
  11. RS256 vs HS256 簽名 – 哪種方式更好?
  12. JWKS (JSON Web Key Set) 端點(Endpoints);
  13. 如何實現JWT簽名的週期性鍵旋轉(Periodic Key Rotation);
  14. JWT在企業應用中的使用;
  15. 歸納和結論;
  • JWT是什麼?

JSON Web Token (or JWT)只是一個包含某種意義資料的JSON串。它最重要的特性就是,為了確認它是否有效,我們只需要看JWT本身的內容,而不需要藉助於第三方服務或者在多個請求之間將其儲存在記憶體中-這是因為它本身攜帶了資訊驗證碼MAC(Message Authentication Code)。

一個JWT包含3個部分:頭部Header,資料Payload,簽名Signature。讓我們逐個來了解一下,先從Payload開始吧。

  • JWT Payload看起來是怎樣的呢?

Payload只是一個普通的Javascript 物件。對於payload的內容,JWT是沒有任何限制的,但必須注意的是,JWT是沒有加密的。因此,任何放在token裡面的資訊,如果被截獲了,對任何人別人是可讀的。因此,我們不應該在Payload裡面存放任何黑客可以利用的使用者資訊。

  • JWT Header – 為什麼是必須的?

Payload的內容在接收者端是通過簽名(Signature)來校驗的。不過存在多種型別的簽名,因此,接收者需要知道使用的是哪種型別的簽名。

這種關於token本身的後設資料資訊存放在另外的Javascript物件裡面,並隨著Payload一起傳送給客戶。這個獨立的物件就是一個JSON物件,叫JWT Header,它也是普通的Javascript物件,在這裡面我們可以看到簽名型別資訊,比如RS256。

  • JWT signatures – 如何被使用來完成認證的?

JWT的最後一部分是簽名,它也叫資訊驗證碼MAC。簽名只能由擁有Payload、Header和金鑰的角色生成。

那簽名是如何完成認證功能的呢,且看:

  1. 使用者向認證伺服器提交使用者名稱和密碼,認證伺服器也可以和應用伺服器部署在一起,但往往是獨立的居多;
  2. 認證伺服器校驗使用者名稱和密碼組合,然後建立一個JWT token,token的Payload裡面包含使用者的身份資訊,以及過期時間戳;
  3. 認證伺服器使用金鑰對Header和Payload進行簽名,然後傳送給客戶瀏覽器;
  4. 瀏覽器獲取到經過簽名的JWT token,然後在之後的每個HTTP請求中附帶著傳送給應用伺服器。經過簽名的JWT就像一個臨時的使用者憑證,代替了使用者名稱和密碼組合,之後都是JWT token和應用伺服器打交道了;
  5. 應用伺服器檢查JWT簽名,確認Payload確實是由金鑰擁有者簽過名的;
  6. Payload身份資訊代表了某個使用者;
  7. 只有認證伺服器擁有私鑰,並且認證伺服器只把token發給提供了正確密碼的使用者;
  8. 因此應用伺服器可以認為這個token是由認證伺服器頒發的也是安全的,因為該使用者具有了正確的密碼;
  9. 應用伺服器繼續完成HTTP請求,並認為這些請求確實屬於這個使用者;

這樣的話,黑客假扮合法使用者的辦法要麼是盜到了使用者名稱和密碼組合,要麼盜到了認證伺服器上的簽名私鑰。

正如我們所見,簽名的確是JWT的關鍵部分!

簽名使得無狀態的伺服器只需要通過檢視HTTP請求中的JWT token就能保證HTTP請求是來自某個使用者,而不需要每次請求時都傳送密碼。

  • JWT的目標是讓伺服器無狀態?

實際上,JWT真正的好處是讓認證伺服器和校驗JWT token的應用伺服器可以完全分開,而讓伺服器無狀態化只是它的一個副作用罷了。這意味著應用伺服器只需要最簡單的認證邏輯-校驗JWT!我們可以將整個應用叢集的登入/註冊委託給一個單獨的認證伺服器。這也意味著應用伺服器更簡單更安全,因為更多的認證功能集中部署在認證伺服器,可以被跨應用使用。

到此,我們從更高的層面瞭解了JWT是怎樣完成無狀態的第三方認證,接下來讓我們瞭解它的實現細節。

  • JSON Web Token看起來是怎樣的呢?

讓我們來看看JWT的實際例子,這是從jwt.io的JWT校驗工具得到的:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

你可能會想,JSON物件去哪了啊?不過,你會馬上找回它。

我們可以看到,這個JWT包含3部分,是由“.”號分開的。第一部分是JWT的Header:

JWT Header:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

第二部分是Payload:

JWT Payload:


eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

第三方部分是簽名Signature:

JWT Signature:

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

如果你還想確認這些資訊是否真的存在,可以拷貝JWT串到jwt.io的線上校驗工具校驗一下即可。

但這些字串代表什麼意思呢?我們如何取回JWT的資訊?

  • Base64,抑或是Base64Url?

不管你信不信,現在的Payload,Header和Signature還是可讀的。這只是因為我們不想在網路傳輸過程中出現一些垃圾文字,比如這樣的字串:qîüö:Ã。

這是因為世界上不同的計算機以不同的編碼方式處理字串,比如UTF-8,ISO 8859-1,等等。因此,這種問題到處存在,只要我們在某個平臺上用到一個字串,它總是使用了某種編碼方式,即使我們沒有顯示指定:

  1. 作業系統的預設編碼方式;
  2. 伺服器上的配置的編碼引數;

我們希望在網路上傳輸字串時沒有這些問題,那就需要選擇這些字元的一個子集,對於這個子集,幾乎所有的編碼方式都是一樣的處理方式,這就是Base64編碼方式產生的原因。

  • Base64 vs Base64Url

但是我們在JWT看到的並不是Base64,實際上是Base64Url,它和Base64類似,但有一些字元不一樣,因此我們可以將JWT作為URL的引數進行傳遞。

我們看一下Payload部分:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

我們使用線上解碼器來解析它,就得到了一個JSON物件,因此,我們可以得到這樣的結論:JWT Header和Payload的內容是普通的javascript物件,轉換成JSON並進行Base64Url編碼,以“.”號隔開。

在學習簽名Signature之前,我們先來看看在實際的使用者認證案例中,我們是將哪些內容放入Payload中的。

  • 基於JWT的使用者會話管理主題和期限

之前有提到,JWT的Payload理論上可以存放任何內容,不一定是使用者身份資訊,只不過使用JWT作為認證是最常用的方式。Payload還有一些特定的屬性來支援:

  1. 使用者身份
  2. 會話過期

這裡是Payload的幾個最常用的標準屬性:

· iss 代表生成token的實體,一般就是認證伺服器

· iat 建立JWT的時間戳(in seconds since Epoch)

· sub 包含使用者的身份資訊

· exp token的過期時間戳

我們把這叫做Bearer Token,意思是:

認證伺服器確認這個token的持有者是具有由sub屬性表示的ID的使用者,因此可以放行。

現在我們理解了Payload在使用者認證中是如何使用的,接下來我們來了解一下簽名Signature。對於JWT,簽名方式有很多種,這裡我們主要了解HS256和RS256。

我們先來看看HS256.

  • HS256 JWT數字簽名 – 它是如何工作的?

和很多簽名方法一樣,HS256 數字簽名基於一種特殊的函式:加密雜湊函式。

這聽起來有點唬人,不過是個值得學習的概念:這個知識已經被使用了20多年並還會持續很長時間。很多關於安全的實現都圍繞著雜湊,它在Web安全中隨處可見。

我們將分為兩步,首先要了解什麼是雜湊函式,然後再看通過這樣的函式和密碼,如何生成資訊認證碼(Message Authentication Code),也就是數字簽名。

  • 什麼是雜湊函式(Hashing function)?

雜湊函式是一種特殊的函式:它在數字簽名中有很多實際的使用案例。現在我們將談論它四個有趣的屬性,然後看看這些屬性如何使得我們可以生成可校驗的簽名。這裡我們將使用的雜湊函式是:SHA-256。

  • 雜湊函式屬性 1 – 不可逆性

雜湊函式有點類似絞肉機:你把牛排放入一端,然後從另一端得到漢堡包,你再也無法從漢堡包取回牛排了。因此,函式是完全不可逆的。這就意味著我們把Header和Payload作用於這個函式後,沒有人可以從函式輸出的資訊中取回Header和Payload的原始值。

使用線上的雜湊計算器,我們可以看到SHA-256的一個輸出值如下:

3f306b76e92c8a8fbae88a3ef1c0f9b0a81fe3a953fa9320d5d0281b059887c3

同時,雜湊並不是加密,加密在定義中是可逆的,我們總是需要從加密後的資訊中得到原始資訊。

  • 雜湊函式屬性 2 – 可重複生成

另外一個需要知道的是,雜湊函式是可重複生成資訊的,也就是如果我們輸入同樣的Header和Payload資訊,每次得到的結果是完全一樣的。這就意味著,給定輸入組合和雜湊輸出值,我們總是可以校驗該輸出值(比如簽名signature)的正確性,因為我們可以重新計算(我們有輸入值的情況下)。

  • 雜湊函式屬性 3 – 沒有衝突

還有一個屬性是,如果我們提供不同的輸入值,總是得到不同的唯一的輸出值。這就意味著我們將雜湊函式作用於某個Payload和Header之後,總是得到相同的結果,其它輸入值組合不會得到和這一樣的結果,因此,雜湊函式的不同輸出值就代表了輸入值的不同。

  • 雜湊函式屬性 4 – 不可預測性

雜湊函式的最後一個屬性就是不可預測性,給定一個輸出值,無法通過各種手段猜測到輸入值。假設我們嘗試從上面的輸出值中找到生成它的Payload,我們只能猜測輸入值然後對比輸出值看看是否匹配。

但對於雜湊函式來說,這種方式是不可行的:

這是因為在雜湊函式中,即使你改變了一個輸入字元甚至一個位元值,輸出中一般有50%的位元值都會被改變,輸入值小小的變動,可能會得到完全不同的輸出值。

這些聽起來挺有趣的,不過你可能又在想了:雜湊函式是怎樣完成數字簽名的呢?黑客是否可以拿著Header和Payload,而不管Signature呢?任何人都可以使用SHA-256雜湊函式生成一個輸出,然後附加到JWT的signature部分,對吧?

  • 怎樣使用雜湊函式生成簽名?

這是正確的,任何人都可以使用雜湊函式,然後輸入Header和Payload來生成結果。但HS256簽名不止這樣,我們拿到Header、Payload外,還要加上一個密碼,將這三個輸入值一起雜湊。輸出結果是一個SHA-256 HMAC或者基於雜湊的MAC。如果需要重複生成,則需要同時擁有Header、Payload和密碼才可以。這也意味著,雜湊函式的輸出結果是一個數字簽名,因為輸出結果就表示了Payload是由擁有密碼的角色生成並加簽了的,沒有其它方式可以生成這樣的輸出值了。

將雜湊結果附加到訊息上,是為了讓接收者可以驗證。雜湊結果叫HMAC:Hash-Based Message Authentication Code,是數字簽名的一種形式。這就是我們在JWT中所做的,JWT的第三部分是由Header、Payload通過SHA-256函式生成,並使用Base64Url進行編碼。

  • 如何校驗JWT簽名?

當我們的服務接收到HS256簽名的JWT時,我們需要使用同樣的密碼才能校驗並確認token裡面的Payload是否有效。為了驗證簽名,我們只需要將JWT Header和Payload以及密碼通過雜湊函式生成結果。如果是使用HS256函式,JWT的接收者需要拿到和傳送者一樣的密碼值。如果我們得到的雜湊結果和JWT第三部分的簽名值是一致的,則說明有效,可以確認傳送者確實和接收者擁有相同的密碼值。

而數字簽名和HMAC又是如何工作的呢?

  • 手動確認SHA-256 JWT簽名

我們從之前的JWT中去掉簽名和第二個“.”,只留下Header和Payload部分。看起來如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

現在如果你拷貝這個字串到線上的HMAC SHA-256工具,並使用上密碼,就可以取回JWT簽名。或者,你會得到Base64編碼後的內容,後面還有一個“=”字元,這算是已經接近Base64Url: 

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ=

那個“=”在URL欄中會顯示為“%3D”,會顯得混亂,這也解釋了在我們把JWT拼接到URL傳送時,需要Base64Url的原因。

  • 為什麼需要其它的簽名型別呢?

以上解釋了JWT簽名是如何應用於認證的,HS256只是一種具體的簽名型別。其它的簽名型別中,最常用的是:RS256。

有什麼區別呢?我們介紹HS256只是為了更容易理解MAC碼的概念, 你可能也會發現它在一些生產環境的應用中被使用。但是一般來說,使用RS256簽名方式會更好,下一節我們將看到,RS256相對於HS256來說有諸多優勢。

  • HS256簽名方式的劣勢

如果輸入的密碼相對弱的話,HS256可能會被暴力破解,基於金鑰的技術都有這個問題。更甚的是,HS256要求JWT的生產者和消費者都預先擁有相同的密碼。

  • 不切實際的密碼分發

這意味著我們在修改密碼後,需要把它分發並安裝到所有需要它的網路節點。這不僅不方便,而且容易出錯,還涉及到伺服器間的協調和暫停服務問題。如果伺服器是由另外的團隊維護,比如第三方組織,這種方式就更不可行了。

  • Token的建立和校驗沒有分離

建立和校驗JWT的能力沒有區分開,使用HS256時,網路的任何人都可以建立和校驗token,因為他們都有密碼。這就意味著密碼可能會從更多的地方丟失或者受攻擊,因為密碼到處分發,而並不是每個應用都具有一樣的安全保護機制。

彌補這問題的一個方法是,建立一個共享的密碼給每一種型別的應用。不過,我們馬上要學習新的簽名方式,這個簽名方式解決了以上所有的問題,並且目前所有基於JWT的方案都預設使用的,那就是RS256。

  • RS256 JWT簽名

使用RS256我們同樣需要生成一個MAC,其目的仍然是建立一個數字簽名來證明一個JWT的有效性。只是在這種簽名方式中就,我們將建立token和校驗token的能力分開,只有認證伺服器具備建立的能力,而應用伺服器,具備校驗的能力。 

這樣,我們需要建立兩個金鑰而不是一個:

  1. 仍然需要一個私鑰,不過這次它只能被認證伺服器擁有,只用來簽名JWT。
  2. 私鑰只能用來簽名JWT,不能用來校驗它。
  3. 第二個金鑰叫做公鑰(public key),是應用伺服器使用來校驗JWT。
  4. 公鑰可以用來校驗JWT,但不能用來給JWT簽名。
  5. 公鑰一般不需要嚴密保管,因為即便黑客拿到了,也無法使用它來偽造簽名。
  • RSA加密技術介紹

RS256使用一種特殊的金鑰,叫RSA金鑰。RSA是一種加解密演算法,使用一個金鑰進行加密,然後用另外一個金鑰解密。值得注意的是,RSA不是雜湊函式,從定義上來說,這種方式加密是可逆的,也就是我們可以從加密後的內容得到原始內容。

來看一下RSA公鑰是怎樣的:

—–BEGIN PUBLIC KEY—–

MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB

—–END PUBLIC KEY—–  

乍一看有點古怪,但它是使用命令列工具比如openssl 或者線上的RSA金鑰生成工具來生成的。

這個公鑰是公開發布的,因此黑客根本不需要猜測,他本來就可以擁有它。

但這裡還有一個RSA私鑰:

—–BEGIN RSA PRIVATE KEY—–

MIICWwIBAAKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQABAoGAD+onAtVye4ic7VR7V50DF9bOnwRwNXrARcDhq9LWNRrRGElESYYTQ6EbatXS3MCyjjX2eMhu/aF5YhXBwkppwxg+EOmXeh+MzL7Zh284OuPbkglAaGhV9bb6/5CpuGb1esyPbYW+Ty2PC0GSZfIXkXs76jXAu9TOBvD0ybc2YlkCQQDywg2R/7t3Q2OE2+yo382CLJdrlSLVROWKwb4tb2PjhY4XAwV8d1vy0RenxTB+K5Mu57uVSTHtrMK0GAtFr833AkEA6avx20OHo61Yela/4k5kQDtjEf1N0LfI+BcWZtxsS3jDM3i1Hp0KSu5rsCPb8acJo5RO26gGVrfAsDcIXKC+bQJAZZ2XIpsitLyPpuiMOvBbzPavd4gY6Z8KWrfYzJoI/Q9FuBo6rKwl4BFoToD7WIUS+hpkagwWiz+6zLoX1dbOZwJACmH5fSSjAkLRi54PKJ8TFUeOP15h9sQzydI8zJU+upvDEKZsZc/UhT/SySDOxQ4G/523Y0sz/OZtSWcol/UMgQJALesy++GdvoIDLfJX5GBQpuFgFenRiRDabxrE9MNUZ2aPFaFp+DyAe+b4nDwuJaW2LURbr8AEZga7oQj0uYxcYw==

—–END RSA PRIVATE KEY—–  

好訊息是,黑客沒有任何辦法猜測私鑰。

而且,這兩個金鑰是相關的,一個金鑰加密的內容只能由另外的金鑰來解密。那我們又如何用它們生成簽名呢?

  • 為什麼不用RSA加密Payload就完了?

現在嘗試著使用RSA來生成一個數字簽名:

我們使用Header和Payload,然後使用私鑰對其進行RSA加密,最後返回JWT。

接收者拿到JWT後,使用公鑰解密,然後檢查解密後的值。如果解密過程順利並且其輸出是一個JSON值,往往就意味著該JWT就是認證伺服器建立並加密了的。

相比雜湊函式,RSA加密過程比較慢。對於資料比較大的Payload來說,可能會是個問題。

那HS256簽名方式在實際中又是如何使用RSA的呢?

  • 使用RSASHA-256簽名JWT (RSA-SHA256)

在實際中,我們一般先將Header和Payload一起雜湊,比如使用SHA-256。這個速度是很快的,這樣我們就得到了一個代表輸入資料的唯一表示,比實際輸入資料要小得多。

然後我們使用RSA對雜湊結果而不是完整的資料(Header+Payload)進行加密,就得到了RS256簽名。我們將這個簽名附加到JWT的第三部分,然後返回給客戶端。

  • 接收者是怎樣檢查RS256簽名的?

接收者將:

  1. 取出Header和Payload,然後使用SHA-256進行雜湊。
  2. 使用公鑰解密數字簽名,得到簽名的雜湊值。
  3. 接收者將解密簽名得到的雜湊值和剛使用Header和Payload參與計算的雜湊值進行比較。如果兩個雜湊值相等,則證明JWT確實是由認證伺服器建立的。

任何人都可以計算雜湊值,但只有認證伺服器可以使用RSA私鑰對其進行加密。

接下來我們學習一下在RS256簽名中遇到問題時的解決思路。

  • 手工確認RS256 JWT簽名

我們看下jwt.io上的例子,一個使用RS256加簽的JWT。

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE

從表面上看,這和HS256 JWT沒有多大區別,但這是使用前面展示的同一個RSA私鑰加簽了的。

我們把簽名部分去掉,只看Header和Payload:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

現在我們需要做的就是使用SHA-256對其進行雜湊,然後使用上面的RSA私鑰進行加密。這樣得到的結果就是JWT簽名,我們可以使用Node的內嵌模組Crypto來確認。 

首先,我們把RSA私鑰儲存到一個文字檔案,比如叫private.key。然後在命令列執行node shell,執行一個小程式,得到下面的結果:

 EkN+DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W/A4K8ZPJijNLis4EZsHeY559a4DFOd50/OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k/4zM3O+vtd1Ghyo4IbqKKSy6J9mTniYJPenn5+HIirE=

這個結果和JWT簽名完全不同,不過等等,這裡面還有斜槓和等號。如果不進一步處理,這是沒法放到URL裡面的。

這是因為我們生成的是Base64 版本的簽名,而我們真正需要的是Base64Url 版本的。我們嘗試轉換一下:

bash$ node

const base64url = require(‘base64url’);

base64url.fromBase64("EkN+DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W/A4K8ZPJijNLis4EZsHeY559a4DFOd50/OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k/4zM3O+vtd1Ghyo4IbqKKSy6J9mTniYJPenn5+HIirE=");

得到下面的結果:

EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE

這就是我們真正想要建立的RS256簽名了!也證明了我們對RS256 JWT簽名的理解是正確的,而且我們知道了在遇到問題時如何去分析解決。

總之,RS256 JWT簽名就是使用RSA對Header和Payload的雜湊值進行加密的結果。現在我們知道RS256簽名是如何工作的了,但這些簽名為什麼好於HS256簽名呢? 

  • RS256簽名 vs HS256 – 為什麼使用RS256?

使用RS256,黑客可以輕鬆實現建立簽名的第一步,即根據盜來的JWT Header和Payload生成SHA-256雜湊值,之後他還要暴力破解RSA才能繼續生成簽名。 

但這還不是我們為什麼選擇RS256而不是HS256的客觀原因。

我們知道,使用RS256時,私鑰只有認證伺服器持有,這就安全得多- 加簽金鑰丟失的風險降低了。然而選擇RS256更重要的理由是-簡化金鑰分發。

  • 如何進行金鑰分發部署

還記得之前我們說過,用來校驗token的公鑰可以隨意分發,黑客無法使用它來做任何有意義的事情。然而黑客並不是想校驗token,他們只是想偽造它們。這就使得我們將公鑰放置到受我們自己控制的伺服器上成為可能。

應用伺服器連線到公鑰放置的伺服器獲取公鑰,然後定期檢查公鑰是否有變化。因此,在更新金鑰時,應用伺服器和認證伺服器不需要同時暫停服務。

那公鑰又如何分發呢?下面是一種可行的格式。

  • JSON Web Key Set Endpoints

有多種釋出公鑰的格式,但這裡有一種較為熟悉:JWKS,全稱Json Web Key Set。

如果你好奇這些endpoints 看起來是怎樣的,可以看一下這個線上例子live example,下面這個是我們從HTT GET請求得到的回覆:

Kid是金鑰身份, x5c是某種公鑰的表示法。這種格式的優點是其標準化,我們只需要知道endpoint的URL,和一個可以解析JWKS的庫,就可以使用公鑰來校驗JWT了,而不需要安裝到自己的伺服器。

JWT常常使用在公共網站上,以及社交產品的登入方案中。對於內部系統,它是怎麼被使用的呢?

  • JWT在企業中的應用

JWT同樣適用於企業內部,替代經典的存在已知安全隱患的預身份驗證設定(Pre-Authentication setup)方式。 

預身份驗證設定方式中,我們的應用伺服器在私有網路的一個代理後面執行,然後從HTTP請求頭中獲取當前使用者資訊。代表使用者身份的HTTP請求頭通常由中心化的登入頁面填充,同時中心化的節點也對使用者session進行管理。

當session過期後,伺服器將阻止對應用的訪問,並要求使用者重新登入認證。之後,它將所有請求轉發到應用伺服器並在HTTP請求頭新增代表使用者身份的資訊。

問題是這種設定方式,內網上的任何人都可以假扮成某個使用者,只要設定同樣的HTTP請求頭。

對此也有一些解決方案,比如白名單列表,或者某種客戶憑證。

  • 更好的預身份驗證設定方式

預身份驗證設定方式是一個好主意,畢竟這種方式可以使得應用開發者不需要實現認證邏輯,減少開發時間和潛在的安全問題。

如果能有預身份驗證設定方式的便捷,又沒有安全方面的妥協,豈不美哉?

如果我們考慮到JWT,則可以輕鬆做到。我們不像以往那樣將使用者名稱放到HTTP請求頭,而是將HTTP請求頭封裝成一個JWT。我們將使用者名稱放到Payload裡面,再由認證伺服器加簽。

應用伺服器不再從HTTP請求頭獲取使用者名稱,而是首先校驗JWT:

  1. 如果簽名是正確的,則使用者認證通過,請求可以放行;
  2. 否則,應用伺服器簡單的拒絕請求就好了;

這樣的結果就是,即使在私有網路內,我們的認證功能也可以正常工作。我們再也不需要通過HTTP請求頭來識別使用者了,我們保證了HTTP請求頭的有效性並且是由代理生成的,而不是某黑客試圖以某個使用者身份登入。

 

  • 總結

通過本文,我們對JWT是什麼有了一個全面的瞭解,以及它是如何在認證中被使用的。JWT只是一個簡單的JSON物件,並且易於驗證、難於偽造。

此外,JWT並不是一定要用來做認證的,我們可以使用JWT在網路上傳送各種資料。

另外一個和安全相關的使用JWT的情況是授權:我們可以在Payload裡面放置使用者的角色列表,比如只讀使用者、管理員等等,對使用者在應用伺服器上的行為進行限制。

好了,本文到此結束,祝你閱讀愉快!

文章來源:想全面理解JWT?一文足矣!

歡迎關注微信公眾號:

相關文章