聊一聊JWT與session

chengkai發表於2017-12-28

前言

認證和授權,其實吧簡單來說就是:認證就是讓伺服器知道你是誰,授權就是伺服器讓你知道你什麼能幹,什麼不能幹,認證授權倆種方式:Session-Cookie與JWT,下面我們就針對這兩種方案就行闡述。

Session

工作原理

當 client通過使用者名稱密碼請求server並通過身份認證後,server就會生成身份認證相關的 session 資料,並且儲存在記憶體或者記憶體資料庫。並將對應的 sesssion_id返回給client,client會把儲存session_id(可以加密簽名下防止篡改)在cookie。此後client的所有請求都會附帶該session_id(畢竟預設會把cookie傳給server),以確定server是否存在對應的session資料以及檢驗登入狀態以及擁有什麼許可權,如果通過校驗就該幹嘛幹嘛,否則重新登入。

前端退出的話就清cookie。後端強制前端重新認證的話就清或者修改session。

聊一聊JWT與session

優勢

相比JWT,最大的優勢就在於可以主動清除session了

session儲存在伺服器端,相對較為安全

結合cookie使用,較為靈活,相容性較好

弊端

cookie + session在跨域場景表現並不好

如果是分散式部署,需要做多機共享session機制,實現方法可將session儲存到資料庫中或者redis中

基於 cookie 的機制很容易被 CSRF

查詢session資訊可能會有資料庫查詢操作

session、cookie、sessionStorage、localstorage的區別

session: 主要存放在伺服器端,相對安全

cookie: 可設定有效時間,預設是關閉瀏覽器後失效,主要存放在客戶端,並且不是很安全,可儲存大小約為4kb

sessionStorage: 僅在當前會話下有效,關閉頁面或瀏覽器後被清除

localstorage: 除非被清除,否則永久儲存

JWT

JSON Web Token(JWT)是一種開放標準(RFC 7519),它定義了一種緊湊且獨立的方式,可以將各方之間的資訊作為JSON物件進行安全傳輸。該資訊可以驗證和信任,因為是經過數字簽名的。

JWT基本上由.分隔的三部分組成,分別是頭部,有效載荷和簽名。 一個簡單的JWT的例子,如下所示:

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ.ec7IVPU-ePtbdkb85IRnK4t4nUVvF2bBf8fGhJmEwSs
複製程式碼

如果你細緻得去看的話會發現其實這是一個分為 3 段的字串,段與段之間用 點號 隔開,在 JWT 的概念中,每一段的名稱分別為:

Header.Payload.Signature
複製程式碼

在字串中每一段都是被 base64url 編碼後的 JSON,其中 Payload 段可能被加密。

Header

JWT 的 Header 通常包含兩個欄位,分別是:typ(type) 和 alg(algorithm)。

  • typ:token的型別,這裡固定為 JWT

  • alg:使用的 hash 演算法,例如:HMAC SHA256 或者 RSA

一個簡單的例子:

    {
      "alg": "HS256",
      "typ": "JWT"
    }
複製程式碼

我們對他進行編碼後是:

    >>> base64.b64encode(json.dumps({"alg":"HS256","typ":"JWT"}))
    'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9'
複製程式碼
Payload

JWT 中的 Payload 其實就是真實儲存我們需要傳遞的資訊的部分,例如正常我們會儲存些使用者 ID、使用者名稱之類的。此外,還包含一些例如釋出人、過期日期等的後設資料。

但是,這部分和 Header 部分不一樣的地方在於這個地方可以加密,而不是簡單得直接進行 BASE64 編碼。但是這裡我為了解釋方便就直接使用 BASE64 編碼,需要注意的是,這裡的 BASE64 編碼稍微有點不一樣,切確得說應該是 Base64UrlEncoder,和 Base64 編碼的區別在於會忽略最後的 padding(=號),然後 '-' 會被替換成'_'。

舉個例子,例如我們的 Payload 是:

 {"user_id":"zhangsan"}
複製程式碼

那麼直接 Base64 的話應該是:

    >>> base64.urlsafe_b64encode('{"user_id":"zhangsan"}')
    'eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ=='
複製程式碼

然後去掉 = 號,最後應該是:

  'eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ'
複製程式碼
Signature

Signature 部分其實就是對我們前面的 Header 和 Payload 部分進行簽名,保證 Token 在傳輸的過程中沒有被篡改或者損壞,簽名的演算法也很簡單,但是,為了加密,所以除了 Header 和 Payload 之外,還多了一個金鑰欄位,完整演算法為:

    Signature = HMACSHA256(
        base64UrlEncode(header) + "." +
        base64UrlEncode(payload),
        secret)
複製程式碼

還是以前面的例子為例,

    base64UrlEncode(header)  =》 eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9
    base64UrlEncode(payload) =》 eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ
複製程式碼

secret 就設為:"secret", 那最後出來的簽名應該是:

    >>> import hmac
    >>> import hashlib
    >>> import base64
    >>> dig = hmac.new('secret',     >>> msg="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ", 
               digestmod=
    >>> base64.b64encode(dig.digest())
    'ec7IVPU-ePtbdkb85IRnK4t4nUVvF2bBf8fGhJmEwSs='
複製程式碼

將上面三個部分組裝起來就組成了我們的 JWT token了,所以我們的

    {'user_id': 'zhangsan'}
複製程式碼

的 token 就是:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiemhhbmdzYW4ifQ.ec7IVPU-ePtbdkb85IRnK4t4nUVvF2bBf8fGhJmEwSs
複製程式碼
工作原理

1.首先,前端通過Web表單將自己的使用者名稱和密碼傳送到後端的介面。這一過程一般是一個HTTP POST請求。建議的方式是通過SSL加密的傳輸(https協議),從而避免敏感資訊被嗅探。

2.後端核對使用者名稱和密碼成功後,將使用者的id等其他資訊作為JWT Payload(負載),將其與頭部分別進行Base64編碼拼接後簽名,形成一個JWT。形成的JWT就是一個形同lll.zzz.xxx的字串。

3.後端將JWT字串作為登入成功的返回結果返回給前端。前端可以將返回的結果儲存在localStorage或sessionStorage上,退出登入時前端刪除儲存的JWT即可。

4.前端在每次請求時將JWT放入HTTP Header中的Authorization位。(解決XSS和XSRF問題)

5.後端檢查是否存在,如存在驗證JWT的有效性。例如,檢查簽名是否正確;檢查Token是否過期;檢查Token的接收方是否是自己(可選)。

6.驗證通過後後端使用JWT中包含的使用者資訊進行其他邏輯操作,返回相應結果。

聊一聊JWT與session

JWTs vs. Sessions

可擴充套件性

隨著應用程式的擴大和使用者數量的增加,你必將開始水平或垂直擴充套件。session資料通過檔案或資料庫儲存在伺服器的記憶體中。在水平擴充套件方案中,你必須開始複製伺服器資料,你必須建立一個獨立的中央session儲存系統,以便所有應用程式伺服器都可以訪問。否則,由於session儲存的缺陷,你將無法擴充套件應用程式。解決這個挑戰的另一種方法是使用 sticky session。你還可以將session儲存在磁碟上,使你的應用程式在雲環境中輕鬆擴充套件。這類解決方法在現代大型應用中並沒有真正發揮作用。建立和維護這種分散式系統涉及到深層次的技術知識,並隨之產生更高的財務成本。在這種情況下,使用JWT是無縫的;由於基於token的身份驗證是無狀態的,所以不需要在session中儲存使用者資訊。我們的應用程式可以輕鬆擴充套件,因為我們可以使用token從不同的伺服器訪問資源,而不用擔心使用者是否真的登入到某臺伺服器上。你也可以節省成本,因為你不需要專門的伺服器來儲存session。為什麼?因為沒有session!

注意:如果你正在構建一個小型應用程式,這個程式完全不需要在多臺伺服器上擴充套件,並且不需要RESTful API的,那麼session機制是很棒的。 如果你使用專用伺服器執行像Redis那樣的工具來儲存session,那麼session也可能會為你完美地運作!

安全性

JWT簽名旨在防止在客戶端被篡改,但也可以對其進行加密,以確保token攜帶的claim 非常安全。JWT主要是直接儲存在web儲存(本地/session儲存)或cookies中。 JavaScript可以訪問同一個域上的Web儲存。這意味著你的JWT可能容易受到XSS(跨站指令碼)攻擊。惡意JavaScript嵌入在頁面上,以讀取和破壞Web儲存的內容。事實上,很多人主張,由於XSS攻擊,一些非常敏感的資料不應該存放在Web儲存中。一個非常典型的例子是確保你的JWT不將過於敏感/可信的資料進行編碼,例如使用者的社會安全號碼。

最初,我提到JWT可以儲存在cookie中。事實上,JWT在許多情況下被儲存為cookie,並且cookies很容易受到CSRF(跨站請求偽造)攻擊。預防CSRF攻擊的許多方法之一是確保你的cookie只能由你的域訪問。作為開發人員,不管是否使用JWT,確保必要的CSRF保護措施到位以避免這些攻擊。

現在,JWT和session ID也會暴露於未經防範的重放攻擊。建立適合系統的重放防範技術,完全取決於開發者。解決這個問題的一個方法是確保JWT具有短期過期時間。雖然這種技術並不能完全解決問題。然而,解決這個挑戰的其他替代方案是將JWT釋出到特定的IP地址並使用瀏覽器指紋。

注意:使用HTTPS / SSL確保你的Cookie和JWT在客戶端和伺服器傳輸期間預設加密。這有助於避免中間人攻擊!

RESTful API服務

現代應用程式的常見模式是從RESTful API查詢使用JSON資料。目前大多數應用程式都有RESTful API供其他開發人員或應用程式使用。由API提供的資料具有幾個明顯的優點,其中之一就是這些資料可以被多個應用程式使用。在這種情況下,傳統的使用session和Cookie的方法在使用者認證方面效果不佳,因為它們將狀態引入到應用程式中。

RESTful API的原則之一是它應該是無狀態的,這意味著當發出請求時,總會返回帶有引數的響應,不會產生附加影響。使用者的認證狀態引入這種附加影響,這破壞了這一原則。保持API無狀態,不產生附加影響,意味著維護和除錯變得更加容易。

另一個挑戰是,由一個伺服器提供API,而實際應用程式從另一個伺服器呼叫它的模式是很常見的。為了實現這一點,我們需要啟用跨域資源共享(CORS)。Cookie只能用於其發起的域,相對於應用程式,對不同域的API來說,幫助不大。在這種情況下使用JWT進行身份驗證可以確保RESTful API是無狀態的,你也不用擔心API或應用程式由誰提供服務。

效能

對此的批判性分析是非常必要的。當從客戶端向伺服器發出請求時,如果大量資料在JWT內進行編碼,則每個HTTP請求都會產生大量的開銷。然而,在會話中,只有少量的開銷,因為SESSION ID實際上非常小。看下面這個例子:

JWT有5個claim:

{

  "sub": "1234567890",

  "name": "Prosper Otemuyiwa",

  "admin": true,

  "role": "manager",

  "company": "Auth0"

}
複製程式碼

編碼時,JWT的大小將是SESSION ID(識別符號)的幾倍,從而在每個HTTP請求中,JWT比SESSION ID增加更多的開銷。而對於session,每個請求在伺服器上需要查詢和反序列化session。

JWT通過將資料保留在客戶端的方式以空間換時間。你應用程式的資料模型是一個重要的影響因素,因為通過防止對伺服器資料庫不間斷的呼叫和查詢來減少延遲。需要注意的是不要在JWT中儲存太多的claim,以避免發生巨大的,過度膨脹的請求。

值得一提的是,token可能需要訪問後端的資料庫。特別是重新整理token的情況。他們可能需要訪問授權伺服器上的資料庫以進行黑名單處理。獲取有關重新整理token和何時使用它們的更多資訊。另外,請檢視本文,瞭解有關黑名單的更多資訊(auth0.com/blog/blackl…)。

下游服務

現代web應用程式的另一種常見模式是,它們通常依賴於下游服務。例如,在原始請求被解析之前,對主應用伺服器的呼叫可能會向下遊伺服器發出請求。這裡的問題是,cookie不能很方便地流到下游伺服器,也不能告訴這些伺服器關於使用者的身份驗證狀態。由於每個伺服器都有自己的cookie方案,所以阻力很大,並且連線它們也是困難的。JSON Web Token再次輕而易舉地做到了!

實效性

此外,無狀態JWT的實效性相比session太差,只有等到過期才可銷燬,而session則可手動銷燬。

例如有個這種場景,如果JWT中儲存有許可權相關資訊,比如當前角色為 admin,但是由於JWT所有者濫用自身權利,高階管理員將權利濫用者的角色降為 user。但是由於 JWT 無法實時重新整理,必需要等到 JWT 過期,強制重新登入時,高階管理員的設定才能生效。

或者是使用者發現賬號被異地登入,然後修改密碼,此時token還未過期,異地的賬號一樣可以進行操作包括修改密碼。

但這種場景也不是沒有辦法解決,解決辦法就是將JWT生成的token存入到redis或者資料庫中,當使用者登出或作出其他想要讓token失效的舉動,可通過刪除token在資料庫或者redis裡面的對應關係來解決這個問題。

node中使用JWT

我這個專案中使用的是JWT,使用方法如下:

首先安裝JWT庫:

npm install jsonwebtoken
複製程式碼

然後建立簽名資料,生成token:

let jwt = require('jsonwebtoken');

var token = jwt.sign({ name: '張三' }, 'shhhhh');
console.log(token);
複製程式碼

執行程式可以看到列印出來的內容類似這樣:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoi5byg5LiJIiwiaWF0IjoxNDYyODgxNDM3fQ.uVWC2h0_r1F4FZ3qDLkGN5KoFYbyZrFpRJMONZrJJog
複製程式碼

之後,對token字串,可以這樣解碼:

let decoded=jwt.decode(token);
console.log(decoded);
複製程式碼

將列印出:

{ name: '張三', iat: 1462881437 }
複製程式碼

其中iat是時間戳,即簽名時的時間(注意:單位是秒)。

不過,一般我們不會使用decode方法,因為它只是簡單的對claims部分的做base64解碼。

我們需要的是驗證claims的內容是否被篡改。

此時我們需要使用verify方法:

let decoded = jwt.verify(token, 'shhhhh');
console.log(decoded);
複製程式碼

雖然列印出的內容和decode方法是一樣的。但是是經過校驗的。

我們可以改變校驗用的金鑰,比如改為shzzzz,使之和加密時的金鑰不一致。那麼解碼就會出現報錯:

JsonWebTokenError: invalid signature
複製程式碼

我們也可以偷偷修改token的claims或者header部分,會得到這樣的報錯:

JsonWebTokenError: invalid token
複製程式碼

最後,根據自己的需求,決定是否需要將生成的token存入資料庫或者redis,但建議不要儲存使用者密碼等敏感資訊。

相關文章