瞭解如何使用JSON Web令牌(JWT)實現訪問授權驗證

banq發表於2019-01-03

JSON Web令牌(JWT)可以輕鬆地在服務(應用程式/站點的內部和外部)之間傳送只讀簽名的 “ 宣告 ” 。宣告是任何一些你希望別人能夠讀取/驗證的資料,但不可改變。

IETF的定義:

“ JSON Web Token (JWT)是一種緊湊的 URL安全 方式,用來表示在不同部分之間進行傳輸的宣告,它可被編碼 為 JSON物件 , 使用JJSON Web Signature (JWS)進行數字 簽名 .“ 

要識別/驗證應用程式中的人員身份,需要在頁面(或API端點)的標頭Header或網址中放置基於標準的令牌,以證明該使用者已登入並允許其訪問所需內容。
例: https://www.yoursite.com/private-content/?token=eyJ0eXAiOiJKV1Qi.eyJrZXkiOi.eUiabuiKv

JWT是一串“url safe”字元,用於編碼資訊,令牌有三個元件(以句點分隔)(此處顯示為多行以便於閱讀,但用作單個文字字串)

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9           // header
.eyJrZXkiOiJ2YWwiLCJpYXQiOjE0MjI2MDU0NDV9      // payload
.eUiabuiKv-8PYk2AkGY4Fb5KMZeorYBLw261JPQD5lM   // signature


1. Header
JWT的第一部分是一個簡單JavaScript物件的編碼字串表示,它描述了令牌以及所使用的雜湊演算法。
2.Payload
JWT的第二部分構成了令牌的核心。Payload有效載荷長度與您在JWT中儲存的資料量成比例。一般的經驗法則是:將最小值儲存在JWT中。
3. Signature簽名
JWT的第三部分也是最後一部分是基於Header(第一部分)和正文(第二部分)生成的簽名,用於驗證 JWT是否有效。

什麼是“宣告”?
宣告是預定義的鍵及其值:

  • iss:令牌的發行者
  • exp:到期時間戳(拒絕已過期的令牌)。注意:如規範中所定義,這必須以秒為單位。
  • iat:JWT釋出的時間。可用於確定JWT的年齡
  • nbf:“not before”是令牌變為活動狀態的未來時間。
  • jti:JWT的唯一識別符號。用於防止JWT被重複使用或重放。
  • sub:令牌的主題(很少使用)
  • aud:令牌的受眾(也很少使用)


案例
一個簡單的例子。(完整原始碼位於/ example目錄中):

https://jwt.herokuapp.com/

該伺服器架構使用node.js http伺服器,我們在/example/server.js中建立了4個端點:

  1. / home:主頁(不是必需的,但我們的登入表單是。)
  2. / auth:驗證訪問者(如果失敗則返回錯誤+登入表單)
  3. / private:我們的受限內容 - 需要登入(有效會話令牌)才能看到此頁面
  4. / logout:使令牌無效並登出使用者(防止重新使用舊令牌)

我們故意讓server.js儘可能簡單:
  • 可讀性
  • 可維護性
  • 可測試性(所有輔助/處理程式方法單獨測試)


幫助方法
所有輔助方法都儲存在/example/lib/helpers.js中 。兩個最有趣/相關的方法是(這裡顯示的簡化版本):

// generate the JWT
function generateToken(req){
  var token = jwt.sign({
    auth:  'magic',
    agent: req.headers['user-agent'],
    exp:   Math.floor(new Date().getTime()/1000) + 7*24*60*60; // Note: in seconds!
  }, secret);  // secret is defined in the environment variable JWT_SECRET
  return token;
}


當使用者認證時產生了JWT令牌(這隨後被髮送回客戶端在授權頭用於後續請求)。
以及:

// validate the token supplied in request header
function validate(req, res) {
  var token = req.headers.authorization;
  try {
    var decoded = jwt.verify(token, secret);
  } catch (e) {
    return authFail(res);
  }
  if(!decoded || decoded.auth !== 'magic') {
    return authFail(res);
  } else {
    return privado(res, token);
  }
}


以上是檢查客戶端提供的JWT是有效的?如果有效則向請求者顯示私有(“privado”)內容,如果不是則呈現authFail 錯誤頁面。
注意:是的,這兩種方法都是同步的。但是,鑑於這些方法都不需要任何 I / O 或 網路請求,因此同步計算它們非常安全。

問題
問:如果我把JWT放在URL或Header中它是否安全?
不安全。除非你使用SSL / TLS來加密連線,明確地傳送令牌始將是不安全的(令牌可以被截獲並重新使用)。一個天真的 “ 緩解 ”方式是向令牌新增可驗證的 “宣告”,例如檢查請求是否來自相同的瀏覽器(使用者代理), IP地址或更高階的“ 瀏覽器指紋 ” http://programmers.stackexchange.com/a/122385
解決的辦法是下面任一個:

  • 使用一次性令牌(在點選連結後就過期失效)
  • 不要使用需要高度安全性的url-tokens。(例如:不要向某人傳送允許他們執行交易的連結)

url中一次性JWT令牌的用例是:
  • 帳戶驗證 - 當您在網站上註冊後透過電子郵件向其傳送連結時。 https://yoursite.co/account/verify?token=jwt.goes.here
  • 密碼重置 - 確保重新設定密碼的人員可以訪問屬於該帳戶的電子郵件。 https://yoursite.co/account/reset-password?token=jwt.goes.here

這兩者都是一次性令牌的好的使用方式(在點選之後到期)。

問:我們如何使會話無效?
使用應用的使用者的裝置(手機/平板電腦/膝上型電腦) 被盜。你如何使他們使用的令牌無效?
JWT背後的想法是令牌是無狀態的, 它們可以由叢集中的任何節點計算,並且在沒有(或慢的)請求資料庫的情況下進行驗證。

將令牌儲存在資料庫中?
1. LevelDB!如果您的應用程式很小或者您不想執行Redis伺服器,則可以透過使用LevelDB獲得Redis的大部分好處:http//leveldb.org/
我們可以任意儲存有效的DB令牌或者 我們可以儲存無效令牌。這兩個都需要往返DB以檢查是否有效/無效。所以我們更喜歡儲存所有令牌,並將令牌的 有效屬性從true更新為false。
儲存在LevelDB中的示例記錄:

"GUID" : {
  "auth" : "true",
  "created" : "timestamp",
  "uid" : "1234"
}

我們將透過其GUID查詢此記錄:

var db = require('level');
db.get(GUID, function(err, record){
  // pseudo-code
  if(record.auth){
    // display private content
  } else {
    // show error message
  }
});


請參閱:example / lib / helpers.js 驗證詳細資訊的方法。

2. Redis
Redis是儲存令牌的可擴充套件方式。

問:返回訪問者(會話之間沒有狀態)
Cookie儲存在客戶端上,並在每次請求時由瀏覽器傳送到伺服器。如果此人關閉瀏覽器,則會保留Cookie,因此可以在停止的位置繼續操作,而無需再次登入。但是,cookie將在與路徑和釋出域匹配的所有請求上傳送,包括不需要的影像和css。

localStorage 提供了一種更好的機制,用於在瀏覽器會話期間和之間儲存令牌。

1.基於瀏覽器的應用程式
儲存JWT有兩種選擇:

  1. 使用localStorage在客戶端儲存JWT(意味著您需要記住在authorization標題中傳送JWT以用於後續的http / ajax請求)
  2. 將您的JWT儲存在cookie中(設定並忘記)

(我們顯然更喜歡無cookie方法。但如果做得好,cookie仍然在現代網路應用程式中佔有一席之地!)

2.程式(API)訪問
訪問您的API的其他服務必須將令牌儲存在檢索系統中(例如:Redis或SQLite用於移動應用程式)並在每個請求時發回令牌。

如何生成金鑰?
由於JSON網路令牌(JWT)不使用非對稱加密簽名,你不能使用ssh-keygen生成使用金鑰,您可以輕鬆使用強密碼,例如: https://www.grc.com/passwords.htm。只要它很長且隨機。碰撞的可能性(因此有人能夠解碼您編碼的JSON)非常低。如果你將兩個強密碼(字串)連線在一起,你將擁有一個128位的ASCII字串。因此,碰撞的可能性小於宇宙中[url=http://en.wikipedia.org/wiki/Observable_universeMatter_content_.E2.80.94_number_of_atoms]的原子數[/url]。

要使用Node的加密庫快速輕鬆地建立金鑰,請執行此命令。

node -e "console.log(require('crypto').randomBytes(32).toString('hex'));"

換句話說,您可以使用RSA金鑰,但你並不需要。
您需要記住的主要事項是:不要與不在核心的人(“ DevOps團隊 ”)共享金鑰,或者在提交給GitHub時突然釋出金鑰!

 

相關文章