理解JSON Web Token (一)

龍恩0707發表於2018-12-09

一:理解單系統登入的原理及實現?

web應用採用的 browser/server 架構的,http是無狀態協議的,也就是說使用者從A頁面跳轉到B頁面會發起http請求,當伺服器返回響應後,當使用者A繼續訪問其他頁面的時候,伺服器端無法獲知該狀態,因此會使用cookie/session來記錄使用者狀態的。

session認證狀態的基本原理:當客戶端向伺服器端請求時,會建立一個session標識存在客戶端的cookie當中,每次請求的時候會將該標識隨cookie一起傳送到伺服器端,伺服器端會首先檢查這個客戶端的請求裡面是否包含了一個session的標識,如果已經包含了,那麼伺服器端就會根據該session標識來判斷使用者的狀態,否則的話,伺服器端會建立一個新的session標識傳給客戶端的cookie當中,以後每次客戶端請求的時候,會從cookie中獲取該標識傳遞過去。

二:理解單點登入的原理?

上面使用session/cookie 可以實現單個系統的登入,那如果是多個系統的話,怎麼辦?難道需要使用者一個個去登入? 一個個去登出?我們需要做的是,無論系統有多少個,我們只需要登入一次就夠了,其他的相關的系統都可以登入/登出一次即可。

單系統登入解決方案的核心是cookie,cookie會攜帶伺服器端返回的sessionId, 在瀏覽器與伺服器端維護會話狀態。但是我們知道cookie是有限制的,cookie有域的概念,瀏覽器傳送http請求時會自動匹配該本站點的cookie域。而不是所有的cookie。

那麼既然這樣,我們很容易想到的是,我們可以把所有子系統的域名都放在一個頂級域名下不就可以了?比如 "*.taobao.com",然後將他們的cookie域設定為 "taobao.com", 但是這種並不好:

第一:因為所有系統的域名需要統一,比如淘寶和天貓的域名就不相同;
第二:應用群各個系統所使用的技術需要相同,比如tomcat伺服器叫JESSIONID, 其他的伺服器可能不叫這個標識。
第三:cookie的安全性不高的。

因此我們需要一種全新的方式來實現多系統應用群的登入,這就是單點登入。

什麼是單點登入?單點登入的全稱是 Single Sign On (可以簡稱為SSO), 在多個系統中只要登入一次,便可以在其他所有系統中得到授權而無需再次登入。

SSO有一個獨立的認證中心,認證中心它可以接受使用者的使用者名稱密碼等安全資訊,其他的地方不接受登入入口,只接受認證中心的間接授權,間接授權它是通過令牌實現的。授權令牌作為引數會傳送到各個子系統,子系統拿到令牌,因此會得到了授權,因此就可以建立了區域性的會話。區域性會話和單系統登入的原理很類似的。

下面我們來打個比方理解單點登入的基本原理:

第一步:我想登入A系統,A系統發現使用者未登入,因此我們需要他們跳轉到SSO認證中心(且將自己請求的地址作為引數傳遞過去)。SSO認證中心發現未登入,會將使用者引導到登入頁面。

第二步:使用者輸入使用者名稱和密碼提交申請登入,SSO認證中心會檢測使用者名稱和密碼資訊,如果使用者名稱和密碼正確的話,那麼使用者和SSO認證中心之間會建立一個區域性會話,並且建立一個授權令牌。sso認證中心會帶著該令牌跳轉到A系統那個請求的地址去。

第三步:A系統會檢測該令牌,如果有效的話,就會跳到使用者輸入的地址頁面去,否則,還是返回登入頁面,提示錯誤資訊。

第四步:使用者訪問系統B,系統B發現使用者未登入,會跳轉到SSO認證中心(將自己請求的地址作為引數傳遞過去),sso認證中心發現使用者已經登入了,會跳轉回系統B的那個地址去,並帶上令牌,系統B拿到令牌,就會去sso認證中心去校驗該令牌是否有效。
如果有效的話,說明認證成功了,就會跳轉到系統B訪問地址的頁面上去。

使用者現在已經登入成功了,sso認證中心會與各個子系統建立會話,使用者與sso認證中心建立的會話被稱為全域性會話,使用者與各個
子系統建立的會話被稱為區域性會話,區域性會話建立之後,使用者訪問子系統資源後就不會再通過sso認證中心了。

三:什麼是JSON Web Token?

JSON Web Token 是一個開放標準協議,它定義了一種緊湊和自包含的方式,它用於各方之間作為JSON物件安全地傳輸資訊。

它有如下優點:

1. 可以適用於分散式的單點登入場景。
2. 可以使用跨域認證解決方案。
3. jwt實現自動重新整理token的方案(待認證)。

JSON Web Token,它定義了一種緊湊和自包含的方式,如何理解緊湊和自包含呢?

緊湊:就是說這個資料量比較少,並且能通過url引數,http請求提交的資料以及http header的方式來傳遞。
自包含:這個串可以包含很多資訊,比如使用者id,訂單號id等,如果其他人拿到該資訊,就可以拿到關鍵業務資訊。

3.1)JWT的基本原理,基本流程如下:

1. 客戶端使用賬號和密碼請求登入介面。
2. 登入成功後伺服器使用簽名金鑰生成JWT,然後返回JWT給客戶端。
3. 客戶端再次向服務端請求其他介面時會帶上JWT。
4. 伺服器接收到JWT後驗證簽名的有效性,對客戶端做出相應的響應。

3.2)JWT與session的區別?

   session是基於cookie來傳輸的,session資訊是儲存在伺服器端中,客戶端向伺服器端發請求時,伺服器端會返回一個jessionId給客戶端中的cookie中,以後每次請求都會從cookie中的jessionid傳遞過去,伺服器通過cookie中的sessionid獲取到當前會話的使用者,對於單系統來講這是沒有問題的,但是對於多個系統的話就涉及到session如何共享的問題了,並且隨著認證使用者增多的話,session會佔用大量伺服器記憶體。

JWT是儲存在客戶端的,伺服器端不需要儲存JWT,JWT含有使用者id,伺服器拿到jwt驗證後就可以拿到使用者資訊了,jwt是無狀態的,它不與任何機器繫結的,只要簽名金鑰足夠的安全就能保證jwt的可靠性。

3.3)JWT中的token與session中的token安全性比較

session 中安全性問題:

伺服器端執行session機制的時候會生成session的口令,在Tomcat伺服器中,預設會採用 jsessionid 這個值,但是在其他伺服器上會有所不同,比如Connect預設會叫 connect_uid, 我們一般把一些敏感的資訊放在cookie中是不可取的,但是將口令放在cookie中還是可以的,如果口令被篡改了的話,就丟失了對映關係,也無法修改伺服器端存在的資料,並且session的有效期一般為20/30分鐘,如果在該時間之內客戶端和伺服器端沒有產生任何互動,伺服器端會自動將session自動清空,因此session中想要維護使用者一直登陸的狀態的話,需要客戶端每隔20分鐘使用setInterval自動發一個請求給伺服器端,這樣的話,前後端就有互動,所以就可以一直保持登陸狀態。否則的話,每次20分鐘後,登陸狀態就會失效,每隔20分鐘使用者需要重新登入,使用者體驗將會變得不好。session這樣做的最主要的是為了安全性考慮,有效期的時間非常短,防止黑客攻擊。

JWT方案中安全性問題

jwt是儲存在客戶端的,伺服器端不需要儲存jwt的,客戶端每次傳送請求時會攜帶該token,然後到伺服器端會驗證token是否正確,是否過期了,然後會通過解碼出攜帶的使用者的資訊的,但是如果token在傳輸的過程中被攻擊者擷取了的話,那麼對方就可以偽造請求,利用竊取到的token模擬正常請求,實現使用者的正常操作,而伺服器端完全不知道,因為JWT在伺服器端是無狀態的,且伺服器端不儲存jwt的。其實jwt解決的問題是認證和授權的問題,對於安全性的話,還是建議對外公佈的介面使用https.

四:理解JWT的基本資料結構

基本的JWT的資料結構是如下這樣的:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia29uZ3poaSIsImlhdCI6MTU0Mzc1MzczNX0.h1XmQo017udxlFsH-8US9Lg8dJ0IDsSbRbjEN5Nq0l4

如上它是由三部分組成的,中間使用 . 分割成三個部分,它是有 Header(頭部), PayLoad(負載),Signature(簽名)組成的。
如:Header.Payload.Signature

4.1 Header

Header部分是一個JSON物件,描述JWT的後設資料,一般是如下的樣子:

{
  "typ": "JWT",
  "alg": "HS256"
}

如上json程式碼,alg屬性表示簽名的演算法,預設是 HMAC SHA256 (縮寫為:HS256); typ屬性表示這個令牌(token)的型別為JWT. 最後將上面的JSON物件使用 Base64URL的演算法轉成字串。
我們可以使用線上的base64編碼轉下(http://tool.oschina.net/encrypt?type=3),如下所示:

如上 alg 部分,預設加密的演算法是 HMAC SHA256, 當然我們也可以選擇下面的加密演算法,加密演算法有如下:

4.2 Payload

Payload部分也是一個JSON物件,用來存放實際需要傳遞的資料,官方提供了7個欄位,如下:

iss(issuer): 簽發人
exp (expiration time): 過期時間
sub (subject): 主題
aud (audience): 受眾
nbf (Not Before): 生效時間
iat (Issued At): 簽發時間
jti (JWT ID): 編號

payload的中文含義是載荷,它可以理解為存放有效資訊的地方。這些有效資訊一般包含如下三個部分:

4.2.1)標準中註冊的宣告:(如上就是官方提供的7個欄位)。

4.2.2)公共的宣告:公共的宣告可以新增任何的資訊,一般新增使用者的相關資訊或其他業務需要的必要資訊,但是不建議新增敏感資訊,
因為該部分在客戶單可解密。

4.2.3)私有的宣告:私有宣告是提供者和消費者所共同定義的宣告,一般不建議存放敏感資訊,因為base64是對稱解密的,該部分資訊
可以理解為明文資訊。

那麼定義一個簡單的 payload 可以如下結構:

{
  "sub": '123456',
  "name": "kongzhi",
  "admin": true
}

我們還是使用如上的base64編碼,會編碼成如下所示:

4.3 Signature

Signature 是對前面兩部分的簽名,防止資料被篡改。簽名是把Header和payload對應的json結構進行base64 編碼之後得到的兩個串用英文句點號拼接起來的,然後會根據header裡面的alg指定的前面演算法(預設是 HMAC SHA256)生成出來的。
如上header部分使用的是 HS256(即HMAC和SHA256),HMAC是用於生成摘要的,SHA256是用於對摘要進行數字簽名的。因此使用HMACSHA256實現signature實現的演算法如下:

HMACSHA256(
  base64UrlEncode(header) + '.' + 
  base64UrlEncode(payload),
  secret
)

如上是 Signature 簽名演算法,最後一個 secret 是加密的金鑰的含義。因此通過如上的用法我們就可以拿到JWT了。

4.4 JWT實踐

JWT的格式是由三個點分割的base64-URL字串,可以在html或http環境中傳遞,我們可以簡單的使用 https://jwt.io/ 官網來生成一個JWT了,如下是我前面定義的部分資料:

五:node中使用JWT的API

nodejs實現的jwt的github程式碼(https://github.com/auth0/node-jsonwebtoken)

它主要有3個方法:

5.1 jwt.sign(payload, secretOrPrivateKey, [options, callback])

payload 引數必須是一個object、Buffer、或 string.
注意:exp(過期時間) 只有當payload是object字面量時才可以設定。如果payload不是buffer或string,它會被強制轉換為使用的字串JSON.stringify()。

secretOrPrivateKey 引數 是包含HMAC演算法的金鑰或RSA和ECDSA的PEM編碼私鑰的string或buffer。

options 引數有如下值:

algorithm:加密演算法(預設值:HS256)
expiresIn:以秒錶示或描述時間跨度zeit / ms的字串。如60,"2 days","10h","7d",含義是:過期時間
notBefore:以秒錶示或描述時間跨度zeit / ms的字串。如:60,"2days","10h","7d"
audience:Audience,觀眾
issuer:Issuer,發行者
jwtid:JWT ID
subject:Subject,主題
noTimestamp
header

該方法如果是非同步方法,則會提供回撥,如果是同步的話,則將會 JsonWebToken返回為字串。
在expiresIn, notBefore, audience, subject, issuer沒有預設值時,可以直接在payload中使用 exp, nbf, aud, sub 和 iss分別表示。

注意:如果在jwts中沒有指定 noTimestamp的話,在jwts中會包含一個iat,它的含義是使用它來代替實際的時間戳來計算的。

下面我們在專案中使用node中jsonwebtoken來生成一個JWT的demo了,在index.js 程式碼如下:

// 生成一個token
const jwt = require('jsonwebtoken');

const secret = 'abcdef';

let token = jwt.sign({
  name: 'kongzhi'
}, secret, (err, token) => {
  console.log(token);
});

然後我們進入專案中的目錄,執行 node index.js 執行後看到命令列中會列印中的token了,如下所示:

當然我們也可以設定token的過期時間,比如設定token的有效期為1個小時,如下程式碼:

// 生成一個token
const jwt = require('jsonwebtoken');

const secret = 'abcdef';
// 設定token為一個小時有效期
let token = jwt.sign({
  name: 'kongzhi',
  exp: Math.floor(Date.now() / 1000) + (60 * 60)
}, secret, (err, token) => {
  console.log(token);
});

5.2 jwt.verify(token, secretOrPrivateKey, [options, callback])

該方法是驗證token的合法性

比如上面生成的token設定為1個小時,生成的token為:

'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia29uZ3poaSIsImlhdCI6MTU0Mzc2MjYzOSwiZXhwIjoxNTQzNzY2MjM5fQ.6idR7HPpjZIfZ_7j3B3eOnGzbvWouifvvJfeW46zuCw'

下面我們使用 jwt.verify來驗證一下:

const jwt = require('jsonwebtoken');
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia29uZ3poaSIsImlhdCI6MTU0Mzc2MjYzOSwiZXhwIjoxNTQzNzY2MjM5fQ.6idR7HPpjZIfZ_7j3B3eOnGzbvWouifvvJfeW46zuCw';
const secret = 'abcdef';
jwt.verify(token, secret, (error, decoded) => {
  if (error) {
    console.log(error.message);
  }
  console.log(decoded);
});

執行node index.js 程式碼後,生成如下資訊:

現在我們再來生成一個token,假如該token的有效期為30秒,30秒後,我再使用剛剛生成的token,再去使用 verify去驗證下,看是否能驗證通過嗎?(理論上token失效了,是不能驗證通過的,但是我們還是來實踐下)。如下程式碼:

// 生成一個token
const jwt = require('jsonwebtoken');

const secret = 'abcdef';
// 設定token為30秒的有效期
let token = jwt.sign({
  name: 'kongzhi',
  exp: Math.floor(Date.now() / 1000) + 30
}, secret, (err, token) => {
  console.log(token);
});

在命令列中生成 jwt為: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia29uZ3poaSIsImV4cCI6MTU0Mzc2MzU1MywiaWF0IjoxNTQzNzYzNTIzfQ.79rH3h_ezayYBeNQ2Wj8fGK_wqsEqEPgRTG9uGmvD64';

然後我們現在使用該token去驗證下,如下程式碼:

// 生成一個token
const jwt = require('jsonwebtoken');

const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia29uZ3poaSIsImV4cCI6MTU0Mzc2MzU1MywiaWF0IjoxNTQzNzYzNTIzfQ.79rH3h_ezayYBeNQ2Wj8fGK_wqsEqEPgRTG9uGmvD64';
const secret = 'abcdef';
jwt.verify(token, secret, (error, decoded) => {
  if (error) {
    console.log(error.message);
  }
  console.log(decoded);
});

執行命令,如下所示:

如上可以看到token的有效期為30秒,30秒後再執行的話,就會提示jwt過期了。

5.3 jwt.decode(token, [, options])
該方法是 返回解碼沒有驗證簽名是否有效的payload。

相關文章