iT邦幫忙

1

Week12 - 要在不同Server間驗證JWT好麻煩嗎?RS256提供你一種簡單的選擇 - JWT篇 [Server的終局之戰系列]

本文章同時發佈於:


最近看反正我很閒有點中毒,我拿他們的影片來比喻JWT驗證的各種情境劇好了ᕕ(ᐛ)ᕗ:

小鐘:一級警報,一級警報。

小樂:什麼一級警報。

小鐘:就Auth-Service發出去的token,為了要讓另一台電腦的Other-Service能夠驗證,所以我把secret key給Other-Service,現在那台電腦被駭了,secret key就被偷了。

小樂:蛤?

小鐘:我們Auth-Service的secret key被偷了。

小樂:被偷了?欸不是,secret key這麼重要,你給Other-Service幹嘛,你為什麼不在Auth-Service上面設計一個驗證token的API不就好了不是嗎,你現在給我被偷是怎樣。

小鐘:之前本來要設計,但你也知道Other-Service每次要驗證都要呼叫Auth-Service的驗證API就很慢嘛。啊Other-Service現在隨便就幾千個偽造token在呼叫,我們secret key真的被偷了。

小樂:不是啊,Other-Service驗證完的結果可以放快取啊,這樣除了第一次驗證要呼叫驗證API,之後直到token過期前都讀快取就好了啊。不是,這種事情你怎麼不早講。

小鐘:我就


JWT是怎麼驗證的?

我們目前較常看到的方式都是HMAC的作法,即是:

透過secret key與使用者資訊做雜湊運算

仔細來說就是,Auth-Service將此token使用哪種演算法與使用者資訊透過Base64編碼成「紅色與紫色的字串」,並使用secret key來與紅色與紫色的字串進行雜湊運算得到「藍色的字串」後,組成token。

程式範例如下,也可點我線上運行:

const jwt = require('jsonwebtoken');
const token = jwt.sign({ user: 'York' }, "IAmSecretKey");

JWT發放出去後,就會有驗證JWT的需求,而驗證方法就是:

驗證方一樣使用secret key來與「紅色與紫色的字串」進行雜湊運算,並將得到的字串與「token的藍色字串」進行比對。程式碼如下,也可點我線上運行:

const jwt = require('jsonwebtoken')
const decoded = jwt.verify(token, 'IAmSecretKey')
console.log(decoded.user)

而小鐘與小樂的討論提供了兩種驗證做法為:

  • 方法一:對驗證方提供secret key
  • 方法二:Auth-Service提供驗證API

而這兩種方法都有以下要注意的地方:

  • 方法一:必須給需要驗證的單位secret key,如果此單位是在我們的「可管控的」,比如說:都在同個機器上的Server,就比較沒有安全問題。但如果是跨機器或者第三方這些「較難管控甚至是不可管控」的單位,要分享此key就成了大問題,因為只要此key一流出,Auth-Service所發的token就會被盜用。
  • 方法二:驗證這件事情由於是交給「可管控」的Auth-Service,所以不必擔心secret key外流的問題,但每次的驗證都需要呼叫一次API,會很浪費時間,所以必須把驗證結果存到快取中,以避免一直無謂的呼叫。

方法二是我目前比較常看到的方法,如果各位有知道不同的方法也歡迎分享討論,不過,這種方法就需要再動到快取的技術,是否有更簡單的方法?那你也許可以考慮RS256。

RS256是什麼

前面所提的HMAC,是被稱為HS256的演算法,做法是:

用「一把」私密的鑰匙來做雜湊運算生產token

RS256則是:

用「兩把」鑰匙分別是公鑰與私鑰以非對稱的方法來簽證token。

以Auth-Service來說,Auth-Service透過私鑰簽證token,驗證方以公鑰來驗證token,如此以來,發放公鑰給「較難管控甚至是不可管控」的單位時,我們不必擔心安全性,因為Auth-Service用來簽證的私鑰從沒外流過。

使用RS256的算法程式碼如下,也可點我線上運行:

const jwt = require('jsonwebtoken')
const privateKey=`-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgHskuZKKc7NL447r40FHHyX3lv8Cf5KybCauK8SRUswnuI3F+e0C
bwtMfjkg0j7wQDH2HCCjEsiwTjXJd9QxWcxb38gdLrpHRftdWaeWYs41aoWJGiBk
DBHjKLqLGzmYhQaGl37XzNUqab/32DdsI1Fme7o9ANEwUPxsEWQvsMMbAgMBAAEC
gYApGWO6Le1ZrP1g6Qeq9MLHmC/UIpBTdKs16bF/5IS+0I7++lFksgg/vCLwjCy/
hs3WHu7aUbLmOjmQKBKPRn1ShdtEKuM5K1pCd7Anj4YLsQjGTRJONNgKw5U9nQiw
YYbvghERLOVPhfab3IPfhYZW7Ye4KmjBjKjU/5zkxHdn+QJBAOYpfSj2hW187atD
I34Fq7ee8DTCElHpkkemgKsPPelG5mYicbSZXePkrp/RPr8DTAVVgm7iBsZP9YLp
h9R0UIUCQQCI966ubKmsLo1T3TLupNeY1mrPl0a9UEDy8tzEaQlFMI9rXgnfXv/n
ZoLG4NPu2CFUemJt6jeVXNMsmFHBF+cfAkBmWwMPKXqy80DazfPFwo3YDfWy8K+m
/+GOvaww5olY6a/iseSxNRc9FuDVr/9ggP3YzWtBFoF+xeZf/qzqPYPlAkEAiBeC
K7GwjXLb3j5lgxWrWyOBka7ADQ8W2c9SaJ3tJiBwAMC5koa0Qtpqiu2N5z49L9FC
x+/3NqO6+A6I/RGhBQJBAK1oJCuv9sl1EWRoLOpr3THcIV3xL3jHyckt7EpNBaTT
Upkj9+K/+wNwjNXvlPvYRjuLn5M83NGsuBCWL+h+ZL8=
-----END RSA PRIVATE KEY-----`
const token = jwt.sign({ user: 'York' }, privateKey, { algorithm: 'RS256' });

而驗證的話我們就要用公鑰來驗證,也可點我線上運行:

const cert = `-----BEGIN PUBLIC KEY-----
MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgHskuZKKc7NL447r40FHHyX3lv8C
f5KybCauK8SRUswnuI3F+e0CbwtMfjkg0j7wQDH2HCCjEsiwTjXJd9QxWcxb38gd
LrpHRftdWaeWYs41aoWJGiBkDBHjKLqLGzmYhQaGl37XzNUqab/32DdsI1Fme7o9
ANEwUPxsEWQvsMMbAgMBAAE=
-----END PUBLIC KEY-----`
const decoded = jwt.verify(token, cert)
console.log(decoded.user)

HS256與RS256的比較

RS256用了很簡單的方法解決了不同單位的驗證需求,但為什麼比較少聽到呢?以下是我的兩種猜測:

  1. RS256是「非對稱」演算法,比起HS256簡單的「雜湊」演算法,速度稍慢了一點。
  2. 許多服務沒有到太複雜的驗證情境,因此也沒有要分享key來驗證的需求。

雖然是猜測,但對於第一點我們還是可以驗證看看,我們以Benchmark.js來效能測試此兩種方法,也可點我線上運行:

const Benchmark = require('benchmark')
const jwt = require('jsonwebtoken')
const suite = new Benchmark.Suite

suite
  .add('HS256', function() {
    const token = jwt.sign({ user: 'York' }, "IAmSecretKey")
    const decoded = jwt.verify(token, 'IAmSecretKey')
  })
  .add('RS256', function() {
    const privateKey=`-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgHskuZKKc7NL447r40FHHyX3lv8Cf5KybCauK8SRUswnuI3F+e0C
bwtMfjkg0j7wQDH2HCCjEsiwTjXJd9QxWcxb38gdLrpHRftdWaeWYs41aoWJGiBk
DBHjKLqLGzmYhQaGl37XzNUqab/32DdsI1Fme7o9ANEwUPxsEWQvsMMbAgMBAAEC
gYApGWO6Le1ZrP1g6Qeq9MLHmC/UIpBTdKs16bF/5IS+0I7++lFksgg/vCLwjCy/
hs3WHu7aUbLmOjmQKBKPRn1ShdtEKuM5K1pCd7Anj4YLsQjGTRJONNgKw5U9nQiw
YYbvghERLOVPhfab3IPfhYZW7Ye4KmjBjKjU/5zkxHdn+QJBAOYpfSj2hW187atD
I34Fq7ee8DTCElHpkkemgKsPPelG5mYicbSZXePkrp/RPr8DTAVVgm7iBsZP9YLp
h9R0UIUCQQCI966ubKmsLo1T3TLupNeY1mrPl0a9UEDy8tzEaQlFMI9rXgnfXv/n
ZoLG4NPu2CFUemJt6jeVXNMsmFHBF+cfAkBmWwMPKXqy80DazfPFwo3YDfWy8K+m
/+GOvaww5olY6a/iseSxNRc9FuDVr/9ggP3YzWtBFoF+xeZf/qzqPYPlAkEAiBeC
K7GwjXLb3j5lgxWrWyOBka7ADQ8W2c9SaJ3tJiBwAMC5koa0Qtpqiu2N5z49L9FC
x+/3NqO6+A6I/RGhBQJBAK1oJCuv9sl1EWRoLOpr3THcIV3xL3jHyckt7EpNBaTT
Upkj9+K/+wNwjNXvlPvYRjuLn5M83NGsuBCWL+h+ZL8=
-----END RSA PRIVATE KEY-----`
    const token = jwt.sign({ user: 'York' }, privateKey, { algorithm: 'RS256' });
    const cert = `-----BEGIN PUBLIC KEY-----
MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgHskuZKKc7NL447r40FHHyX3lv8C
f5KybCauK8SRUswnuI3F+e0CbwtMfjkg0j7wQDH2HCCjEsiwTjXJd9QxWcxb38gd
LrpHRftdWaeWYs41aoWJGiBkDBHjKLqLGzmYhQaGl37XzNUqab/32DdsI1Fme7o9
ANEwUPxsEWQvsMMbAgMBAAE=
-----END PUBLIC KEY-----`
    const decoded = jwt.verify(token, cert)
  })
  .on('cycle', function(event) {
    console.log(String(event.target));
  })
  .on('complete', function() {
    console.log('Fastest is ' + this.filter('fastest').map('name'));
  })
  .run({ 'async': true });

可以看到結果如下,HS256比RS256快了約17倍,

HS256可以一秒執行3萬多次,而RS256一秒執行2千多次,雖然HS256快了不少,但雙方都執行相當快是否需要考慮速度就是一個問題了。

另外,而且如果真的要考慮速度,HS256較常看到的作法是

第一次呼叫Auth-Service的驗證API,並且結果存快取,之後再讀取快取的驗證結果

這個呼叫API的步驟,肯定是比純運算來得久非常多,所以HS256的實作上又不一定比RS256快了,並且RS256的結果也是能存快取的,那麼RS256因為都不用呼叫驗證API,所以又反過來比HS256快了(ノ゚▽゚)ノ。

在比較上面,國外也有人詢問了此問題,Val_Melamed認為RS256很安全,但為什麼大家都常用HS256呢?

而Auth0(專門在做身份認證與授權的廠商)的員工是這樣回答的:

就是:「沒有啦,我們現在是用RS256來當預設的token演算法呀(´∀`)」


所以說,RS256還是提供了一個不錯的方法來驗證JWT,是可以考慮的,而較傳統的HS256也沒有錯,就取決於團隊想要用哪種作法。

而你會用哪種作法呢?歡迎分享與討論供大家參考~

小提醒

這是我當初對JWT的一個誤解,那時我一直以為token是全部的字串都是加密的,事實上只有最後的藍色字串是有被雜湊演算或者非對稱簽證的,紅色與紫色的字串是Base64編碼並不是加密,他只是將資訊轉換成Base64的格式,他是可以直接轉回來的

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiWW9yayJ9.rgmKsHCtvBZRjOCjULB8aMyMtqP8VkWiz3__WO_QXnY

大家可以複製前兩段字串在此網站嘗試解碼,會得到以下結果:


謝謝你的閱讀,也歡迎分享討論~


尚未有邦友留言

立即登入留言