前言
認證和授權,其實吧簡單來說就是:認證就是讓伺服器知道你是誰,授權就是伺服器讓你知道你什麼能幹,什麼不能幹,認證授權倆種方式: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了
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中包含的使用者資訊進行其他邏輯操作,返回相應結果。
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,但建議不要儲存使用者密碼等敏感資訊。