JWT、JWE、JWS 、JWK 到底是什麼?該用 JWT 還是 JWS?

語言: CN / TW / HK

JWT 相信很多小夥伴都知道,JSON Web Token,如果在專案中通過 jjwt 來支援 JWT 的話,可能只需要瞭解 JWT 一個概念即可,但是現在很多時候我們可能不是使用 jjwt,而是選擇 nimbus-jose-jwt 庫,此時就有可能接觸到一些新的概念,如 JWE、JWS。那麼 JWE、JWS 以及 JWT 之間是什麼關係呢?松哥最近看到一篇不錯的文章講這個,我們一起來看下,以下是正文。

什麼是 JWT

一個JWT,應該是如下形式的:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.  
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.  
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ 

這些東西看上很凌亂,但是非常緊湊,並且是可列印的主要用於驗證簽名的真實性。

JWT 解決什麼問題?

JWT的主要目的是在服務端和客戶端之間以安全的方式來轉移宣告。主要的應用場景如下所示:

  1.  認證 Authentication;
  2.  授權 Authorization // 注意這兩個單詞的區別;
  3.  聯合識別;
  4.  客戶端會話(無狀態的會話);
  5.  客戶端機密。

JWT 的一些名詞解釋

  1.  JWS:Signed JWT簽名過的jwt
  2.  JWE:Encrypted JWT部分payload經過加密的jwt;目前加密payload的操作不是很普及;
  3.  JWK:JWT的金鑰,也就是我們常說的 scret;
  4.  JWKset:JWT key set在非對稱加密中,需要的是金鑰對而非單獨的金鑰,在後文中會闡釋;
  5.  JWA:當前JWT所用到的密碼學演算法;
  6.  nonsecure JWT:當頭部的簽名演算法被設定為none的時候,該JWT是不安全的;因為簽名的部分空缺,所有人都可以修改。

JWT的組成

一個通常你看到的jwt,由以下三部分組成,它們分別是:

  1.  header:主要聲明瞭JWT的簽名演算法;
  2.  payload:主要承載了各種宣告並傳遞明文資料;
  3.  signture:擁有該部分的JWT被稱為JWS,也就是簽了名的JWS;沒有該部分的JWT被稱為nonsecure JWT 也就是不安全的JWT,此時header中宣告的簽名演算法為none。

三個部分用·分割。形如 xxxxx.yyyyy.zzzzz的樣式。

JWT header

{  
  "typ": "JWT",  
  "alg": "none",  
  "jti": "4f1g23a12aa"  
} 

jwt header 的組成

頭通常由兩部分組成:令牌的型別,即JWT,以及正在使用的雜湊演算法,例如HMAC SHA256或RSA。

當然,還有兩個可選的部分,一個是jti,也就是JWT ID,代表了正在使用JWT的編號,這個編號在對應服務端應當唯一。當然,jti也可以放在payload中。

另一個是cty,也就是content type。這個比較少見,當payload為任意資料的時候,這個頭無需設定,但是當內容也帶有jwt的時候。也就是巢狀JWT的時候,這個值必須設定為jwt。這種情況比較少見。

jwt header 的加密演算法

加密的方式如下:

base64UrlEncode(header)  
>> eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIiwianRpIjoiNGYxZzIzYTEyYWEifQ 

JWT payload

{  
  "iss": "http://shaobaobaoer.cn",  
  "aud": "http://shaobaobaoer.cn/webtest/jwt_auth/",  
  "jti": "4f1g23a12aa",  
  "iat": 1534070547,  
  "nbf": 1534070607,  
  "exp": 1534074147,  
  "uid": 1,  
  "data": {  
    "uname": "shaobao",  
    "uEmail": "[email protected]",  
    "uID": "0xA0",  
    "uGroup": "guest"  
  }  
} 

jwt payload的組成payload通常由三個部分組成,分別是 Registered Claims ; Public Claims ; Private Claims ;每個宣告,都有各自的欄位。

Registered Claims

  •  iss  【issuer】釋出者的url地址
  •  sub 【subject】該JWT所面向的使用者,用於處理特定應用,不是常用的欄位
  •  aud 【audience】接受者的url地址
  •  exp 【expiration】 該jwt銷燬的時間;unix時間戳
  •  nbf  【not before】 該jwt的使用時間不能早於該時間;unix時間戳
  •  iat   【issued at】 該jwt的釋出時間;unix 時間戳
  •  jti    【JWT ID】 該jwt的唯一ID編號

Public Claims 這些可以由使用JWT的那些標準化組織根據需要定義,應當參考文件IANA JSON Web Token Registry。

Private Claims 這些是為在同意使用它們的各方之間共享資訊而建立的自定義宣告,既不是註冊宣告也不是公開宣告。上面的payload中,沒有public claims只有private claims。

jwt payload 的加密演算法

加密的方式如下:

base64UrlEncode(payload)  
>>eyJpc3MiOiJodHRwOi8vc2hhb2Jhb2Jhb2VyLmNuIiwiYXVkIjoiaHR0cDovL3NoYW9iYW9iYW9lci5jbi93ZWJ0ZXN0L2p3dF9hdXRoLyIsImp0aSI6IjRmMWcyM2ExMmFhIiwiaWF0IjoxNTM0MDcwNTQ3LCJuYmYiOjE1MzQwNzA2MDcsImV4cCI6MTUzNDA3NDE0NywidWlkIjoxLCJkYXRhIjp7InVuYW1lIjoic2hhb2JhbyIsInVFbWFpbCI6InNoYW9iYW9iYW9lckAxMjYuY29tIiwidUlEIjoiMHhBMCIsInVHcm91cCI6Imd1ZXN0In19 

暴露的資訊

所以,在JWT中,不應該在載荷裡面加入任何敏感的資料。在上面的例子中,我們傳輸的是使用者的User ID,郵箱等。這個值實際上不是什麼敏感內容,一般情況下被知道也是安全的。但是像密碼這樣的內容就不能被放在JWT中了。如果將使用者的密碼放在了JWT中,那麼懷有惡意的第三方通過Base64解碼就能很快地知道你的密碼了。

當然,這也是有解決方案的,那就是加密payload。在之後會說到

JWS 的概念

JWS 的結構

JWS ,也就是JWT Signature,其結構就是在之前nonsecure JWT的基礎上,在頭部宣告簽名演算法,並在最後新增上簽名。建立簽名,是保證jwt不能被他人隨意篡改。

為了完成簽名,除了用到header資訊和payload資訊外,還需要演算法的金鑰,也就是secret。當利用非對稱加密方法的時候,這裡的secret為私鑰。

為了方便後文的展開,我們把JWT的金鑰或者金鑰對,統一稱為JSON Web Key,也就是JWK。

jwt signature 的簽名演算法

RSASSA || ECDSA || HMACSHA256(  
  base64UrlEncode(header) + "." +  
  base64UrlEncode(payload),  
  secret)  
>>GQPGEpixjPZSZ7CmqXB-KIGNzNl4Y86d3XOaRsfiXmQ  
>># 上面這個是用 HMAC SHA256生成的 

到目前為止,jwt的簽名演算法有三種。

  •  對稱加密HMAC【雜湊訊息驗證碼】:HS256/HS384/HS512
  •  非對稱加密RSASSA【RSA簽名演算法】(RS256/RS384/RS512)
  •  ECDSA【橢圓曲線資料簽名演算法】(ES256/ES384/ES512)

最後將簽名與之前的兩段內容用.連線,就可以得到經過簽名的JWT,也就是JWS。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjRmMWcyM2ExMmFhIn0.eyJpc3MiOiJodHRwOi8vc2hhb2Jhb2Jhb2VyLmNuIiwiYXVkIjoiaHR0cDovL3NoYW9iYW9iYW9lci5jbi93ZWJ0ZXN0L2p3dF9hdXRoLyIsImp0aSI6IjRmMWcyM2ExMmFhIiwiaWF0IjoxNTM0MDcwNTQ3LCJuYmYiOjE1MzQwNzA2MDcsImV4cCI6MTUzNDA3NDE0NywidWlkIjoxLCJkYXRhIjp7InVuYW1lIjoic2hhb2JhbyIsInVFbWFpbCI6InNoYW9iYW9iYW9lckAxMjYuY29tIiwidUlEIjoiMHhBMCIsInVHcm91cCI6Imd1ZXN0In19.GQPGEpixjPZSZ7CmqXB-KIGNzNl4Y86d3XOaRsfiXmQ 

當驗證簽名的時候,利用公鑰或者金鑰來解密Sign,和 base64UrlEncode(header) + "." + base64UrlEncode(payload) 的內容完全一樣的時候,表示驗證通過。

JWS 的額外頭部宣告

如果對於CA有些概念的話,這些內容會比較好理解一些。為了確保伺服器的金鑰對可靠有效,同時也方便第三方CA機構來簽署JWT而非本機伺服器簽署JWT,對於JWS的頭部,可以有額外的宣告,以下宣告是可選的,具體取決於JWS的使用方式。如下所示:

  •  jku: 傳送JWK的地址;最好用HTTPS來傳輸
  •  jwk: 就是之前說的JWK
  •  kid: jwk的ID編號
  •  x5u: 指向一組X509公共證書的URL
  •  x5c: X509證書鏈
  •  x5t:X509證書的SHA-1指紋
  •  x5t#S256: X509證書的SHA-256指紋
  •  typ: 在原本未加密的JWT的基礎上增加了 JOSE 和 JOSE+ JSON。JOSE序列化後文會說及。適用於JOSE標頭的物件與此JWT混合的情況。
  •  crit: 字串陣列,包含宣告的名稱,用作實現定義的擴充套件,必須由 this->JWT的解析器處理。不常見。

多重驗證與JWS序列化

當需要多重簽名或者JOSE表頭的物件與JWS混合的時候,往往需要用到JWS的序列化。JWS的序列化結構如下所示:

{  
    "payload": "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",  
"signatures":   
    [  
        {  
            "protected": "eyJhbGciOiJSUzI1NiJ9",  
            "header": { "kid": "2010-12-29" },  
            "signature":"signature1"  
        },  
        {  
            "protected": "eyJhbGciOiJSUzI1NiJ9",  
            "header": { "kid": "e9bc097a-ce51-4036-9562-d2ade882db0d" },  
            "signature":"signature2"  
        },  
        ...  
    ]  
} 

結構很容易理解。首先是payload欄位,這個不用多講,之後是signatures欄位,這是一個數組,代表著多個簽名。每個簽名的結構如下:

  •  protected:之前的頭部宣告,利用b64uri加密;
  •  header:JWS的額外宣告,這段內容不會放在簽名之中,無需驗證;
  •  signature:也就是對當前header+payload的簽名。

JWE 相關概念

JWE是一個很新的概念,總之,除了jwt的官方手冊外,很少有網站或者部落格會介紹這個東西。也並非所有的庫都支援JWE。這裡記錄一下自己看官方手冊後理解下來的東西。

JWS是去驗證資料的,而JWE(JSON Web Encryption)是保護資料不被第三方的人看到的。通過JWE,JWT變得更加安全。

JWE和JWS的公鑰私鑰方案不相同,JWS中,私鑰持有者加密令牌,公鑰持有者驗證令牌。而JWE中,私鑰一方應該是唯一可以解密令牌的一方。

在JWE中,公鑰持有可以將新的資料放入JWT中,但是JWS中,公鑰持有者只能驗證資料,不能引入新的資料。因此,對於公鑰/私鑰的方案而言,JWS和JWE是互補的。

JWE 的構成

一個JWE,應該是如下形式的:

eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.  
UGhIOguC7IuEvf_NPVaXsGMoLOmwvc1GyqlIKOK1nN94nHPoltGRhWhw7Zx0-kFm1NJn8LE9XShH59_  
i8J0PH5ZZyNfGy2xGdULU7sHNF6Gp2vPLgNZ__deLKxGHZ7PcHALUzoOegEI-8E66jX2E4zyJKxYxzZIItRzC5hlRirb6Y5Cl_p-ko3YvkkysZIFNPccxRU7qve1WYPxqbb2Yw8kZqa2rMWI5ng8Otv  
zlV7elprCbuPhcCdZ6XDP0_F8rkXds2vE4X-ncOIM8hAYHHi29NX0mcKiRaD0-D-ljQTPcFPgwCp6X-nZZd9OHBv-B3oWh2TbqmScqXMR4gp_A.  
AxY8DCtDaGlsbGljb3RoZQ.  
KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY.  
9hH0vgRfYgPnAHOd8stkvw 

如你所見JWE一共有五個部分,分別是:

  •  The protected header,類似於JWS的頭部;
  •  The encrypted key,用於加密密文和其他加密資料的對稱金鑰;
  •  The initialization vector,初始IV值,有些加密方式需要額外的或者隨機的資料;
  •  The encrypted data (cipher text),密文資料;
  •  The authentication tag,由演算法產生的附加資料,來防止密文被篡改。

JWE 金鑰加密演算法

一般來說,JWE需要對金鑰進行加密,這就意味著同一個JWT中至少有兩種加密演算法在起作用。但是並非將金鑰拿來就能用,我們需要對金鑰進行加密後,利用JWK金鑰管理模式來匯出這些金鑰。JWK的管理模式有以下五種,分別是:

  •  Key Encryption
  •  Key Wrapping
  •  Direct Key Agreement
  •  Key Agreement with Key Wrapping
  •  Direct Encryption

並不是所有的JWA都能夠支援這五種金鑰管理管理模式,也並非每種金鑰管理模式之間都可以相互轉換。可以參考Spomky-Labs/jose中給出的表格(https://github.com/Spomky-Labs/jose/blob/master/doc/operation/Encrypt.md),至於各個金鑰管理模式的細節,還請看JWT的官方手冊,解釋起來較為複雜。

JWE Header

就好像是JWS的頭部一樣。JWE的頭部也有著自己規定的額外宣告欄位,如下所示:

  •  type:一般是 jwt
  •  alg:演算法名稱,和JWS相同,該演算法用於加密稍後用於加密內容的實際金鑰
  •  enc:演算法名稱,用上一步生成的金鑰加密內容的演算法。
  •  zip:加密前壓縮資料的演算法。該引數可選,如果不存在則不執行壓縮,通常的值為 DEF,也就是deflate演算法
  •  jku/jkw/kid/x5u/x5c/x5t/x5t#S256/typ/cty/crit:和JWS額額外宣告一樣。

JWE 的加密過程

步驟2和步驟3,更具不同的金鑰管理模式,應該有不同的處理方式。在此只羅列一些通常情況。

之前談及,JWE一共有五個部分。現在來詳細說一下加密的過程:

  1.  根據頭部alg的宣告,生成一定大小的隨機數;
  2.  根據金鑰管理模式確定加密金鑰;
  3.  根據金鑰管理模式確定JWE加密金鑰,得到CEK;
  4.  計算初始IV,如果不需要,跳過此步驟;
  5.  如果ZIP頭申明瞭,則壓縮明文;
  6.  使用CEK,IV和附加認證資料,通過enc頭宣告的演算法來加密內容,結果為加密資料和認證標記;
  7.  壓縮內容,返回token。 
base64(header) + '.' +base64(encryptedKey) + '.' + // Steps 2 and 3base64(initializationVector) + '.' + // Step 4base64(ciphertext) + '.' + // Step 6base64(authenticationTag) // Step 6 

多重驗證與JWE序列化

和JWS類似,JWE也定義了緊湊的序列化格式,用來完成多種形式的加密。大致格式如下所示:

{  
    "protected": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2In0",  
    "unprotected": { "jku":"https://server.example.com/keys.jwks" },  
    "recipients":[  
        {  
        "header": { "alg":"RSA1_5","kid":"2011-04-29" },  
        "encrypted_key":  
        "UGhIOguC7Iu...cqXMR4gp_A"  
        },  
        {  
        "header": { "alg":"A128KW","kid":"7" },  
        "encrypted_key": "6KB707dM9YTIgH...9locizkDTHzBC2IlrT1oOQ"  
        }  
    ],  
    "iv": "AxY8DCtDaGlsbGljb3RoZQ",  
    "ciphertext": "KDlTtXchhZTGufMYmOYGS4HffxPSUrfmqCHXaI9wOGY",  
    "tag": "Mz-VPPyU4RlcuYv1IwIvzw"  
} 

結構很容易理解,如下所示:

  •  protected:之前的頭部宣告,利用b64uri加密;
  •  unprotected:一般放JWS的額外宣告,這段內容不會被b64加密;
  •  iv:64加密後的iv引數;
  •  add:額外認證資料;
  •  ciphertext:b64加密後的加密資料;
  •  recipients:b64加密後的認證標誌-加密鏈,這是一個數組,每個陣列中包含了兩個資訊;
  •  header:主要是聲明當前金鑰的演算法;
  •  encrypted_key:JWE加密金鑰。

JWT 的工作原理

這裡通過juice shop來說下jwt是如何工作的。在身份驗證中,當用戶使用其憑據成功登入時,將返回JSON Web令牌。如下所示:往此時,返回了jwt的令牌。

每當使用者想要訪問受保護的路由或資源時,使用者將使用承載【bearer】模式傳送JWT,通常在Authorization標頭中。標題的內容應如下所示:

Authorization: Bearer <token> 

隨後,伺服器會取出token中的內容,來返回對應的內容。須知,這個token不一定會儲存在cookie中,如果存在cookie中的話,需要設定為http-only,防止XSS。另外,還可以放在別的地方,比如localStorage、sessionStorage。如果使用vue的話,還可以存在vuex裡面。

另外,如果在如Authorization: Bearer中傳送令牌,則跨域資源共享(CORS)將不會成為問題,因為它不使用cookie。

此時,去訪問認證頁面,請求頭如下所示,如預期所見,是利用Authorization:Bearer的請求頭去訪問的。

ECDSA|RSASSA or HMAC ?應該選用哪個?

之前看JWT的時候看到論壇裡的一個話題,覺得很有意思,用自己的理解來說一下 https://stackoverflow.com/questions/38588319/understanding-rsa-signing-for-jwt

首先,我們必須明確一點,無論用的是 HMAC,RSASSA,ECDSA;金鑰,公鑰,私鑰都不會發送給客戶端,僅僅會保留在服務端上。

對稱的演算法HMAC適用於單點登入,一對一的場景中。速度很快。

但是面對一對多的情況,比如一個APP中的不同服務模組,需要JWT登入的時候,主服務端【APP】擁有一個私鑰來完成簽名即可,而使用者帶著JWT在訪問不同服務模組【副服務端】的時候,副服務端只要用公鑰來驗證簽名就可以了。從一定程度上也減少了主服務端的壓力。

當然,還有一種情況就是不同成員進行開發的時候,大家可以用統一的私鑰來完成簽名,然後用各自的公鑰去完成對JWT的認證,也是一種非常好的開發手段。

因此,構建一個沒有多個小型“微服務應用程式”的應用程式,並且開發人員只有一組的,選擇HMAC來簽名即可。其他情況下,儘量選擇RSA。

【責任編輯:龐桂玉 TEL:(010)68476606】