什麼是JWT?本篇文章帶大家了解JWT,介紹一下JWT在node的應用,以及JWT的優缺點,希望對大家有幫助!
JWT也就是JSON Web Token的縮寫,也就是為了在網路應用環境中一種認證解決方案,在傳統的認證機制中,無非是幾個步驟:
1. 用戶將帳號密碼傳送到伺服器; 2. 伺服器通過驗證帳號密碼後,會在目前session中儲存一些使用者相關的訊息,使用者角色或過期時間等等; 3. 伺服器給使用者一個session_id, 寫入使用者的Cookie或客戶端自行保存在本地; 4. 使用者每次要求服務,都需要帶上這個session_id,或許會透過Cookie,或其他的方式; 5. 伺服器接收到後,回去資料庫查詢目前的session_id,校驗該使用者是否有權限;
這種模式有一種優點在於,伺服器隨時可以終止使用者的權限,可以去資料庫修改或刪除目前使用者的session資訊。但是也有一點不好的,就是如果是伺服器集群的話,所有的機器就需要共享這些session信息,確保每台伺服器都能夠獲取到相同的session存儲信息。雖然可以解決這些問題,但是工程量龐大。
JWT方案的優勢呢,就是不保存這些信息,token資料保存在客戶端,每次接受請求時,只需要校驗就好。
簡單說一下JWT的原理,其實就是客戶端發送請求認證的時候,伺服器在認證用戶之後,會產生一個JSON對象,大概包括“你是誰,你是乾嘛的等等,到期時間”這些信息,重要的是一定要有到期時間;大致格式為:
{ username: "賊煩字串er", role: "世代碼農", endTime: "2022年5月20日" }
但不會用這麼膚淺的方式傳給你,它會透過制定的簽章演算法和你提交的payload的一些資訊進行可逆的簽章演算法進行簽章後傳輸,大致的格式我用一張圖片表示:
由圖片可以看出,傳回的訊息大致分為三部分,左側為簽名之後的結果,也就是傳回給客戶端的結果,右邊也是就Decoded的源碼了,三部分由「點」隔開,分別由紅、紫、青三種顏色一一對應:
第一個紅色部分是Header,Header中主要是指定了的方式,圖中的簽名算法(默認HS256 )就是帶有SHA-256 的HMAC 是一種對稱算法, 雙方之間僅共享一個密鑰,typ字段標識為JWT類型;
第二個紫色部分payload,就是一個JSON對象,也就是實際要傳輸的數據,官方有七個欄位可以使用:
iss (issuer):簽發人
exp (expiration time):過期時間
sub (subject):主題
aud (audience):受眾
nbf (Not Before):生效時間
iat (Issued At):簽發時間
jti (JWT ID):編號
除了這些字段,你還可以搞一些自訂的字段,由於JWT預設是不加密的,所以在使用的時候盡量注意不要使用一些敏感資料。
第三部分就是Signature
簽名,這一部分,是由你自己指定且只有伺服器存在的秘鑰,然後使用頭部指定的演算法透過下面的簽名方法進行簽名。
下面我們來感受一下具體的使用:
第一步:我們需要搭建一個nodejs的專案;透過npm init -y
初始化一個專案;之後我們需要安裝依賴,分別按狀express
、 jsonwebtoken
和nodemon
三個依賴:
$ npm i express jsonwebtoken nodemon
之後在package.json
中的scripts
欄位中加入nodemon app.js
指令:
"scripts": { "start": "nodemon app.js" },
第二步:初始化一下node應用,在根目錄下建立app.js檔案;
// app.js const express = require("express"); const app = express(); app.use(express.json()); app.listen(3000, () => { console.log(3000 + " listening..."); // 監聽3000埠});
第三步:引入jsonwebtoken
依賴,並且建立介面和伺服器的私鑰;
// app.js //... const jwt = require("jsonwebtoken"); const jwtKey = "~!@#$%^&*()+,"; // ...
這裡面的jwtKey
是我們自訂保存僅限保存在伺服器中的私鑰,之後我們開始寫一個/login 接口,用來登錄,並且創建本地的模擬數據庫用來校驗,並通過jwt.sign
方法進行校驗簽名:
// app.js const database = { username: "username", password: "password", }; app.post("/login", (req, res) => { const { username, password } = req.body; if (username === database.username && password === database.password) { jwt.sign( { username, }, jwtKey, { expiresIn: "30S", }, (_, token) => { res.json({ username, message: "登陸成功", token, }); } ); } });
上面程式碼中我們創建了database
變數來模擬創建了本地的帳號密碼資料庫,用來校驗登陸;接下來建立了一個/login
的post
接口,在校驗帳號密碼完全匹配之後,我們透過jsonwebtoken
包導入的jwt
物件下的人sign
方法進行簽名,這個方法有三種介面簽名:
export function sign( payload: string | Buffer | object, secretOrPrivateKey: Secret, options?: SignOptions, ): string; export function sign( payload: string | Buffer | object, secretOrPrivateKey: Secret, callback: SignCallback, ): void; export function sign( payload: string | Buffer | object, secretOrPrivateKey: Secret, options: SignOptions, callback: SignCallback, ): void;
這裡用到了函數重載的方式實作接口,我們這裡將實作最後一個介面簽名,第一個參數可以是一個自訂的物件類型,也可以是一個Buffer
類型,還可以直接是一個string
類型,我們的源碼使用了object
類型,自訂了一些字段,因為jwt在進行簽名是也會對這些數據一併進行簽名,但是值得注意的是,這裡盡量不要使用敏感數據,因為JWT默認是不加密的,它的核心就是簽名,保證資料不會被竄改,而檢查簽名的過程就叫做驗證。
當然你也可以對原始Token進行加密後傳輸;
第二個參數:是我們保存在伺服器用來簽署的秘鑰,通常在客戶端-服務端模式中,JWS 使用JWA 提供的HS256 演算法加上一個金鑰即可,這種方式嚴格依賴金鑰,但在分散式場景,可能多個服務都需要驗證JWT,若要在每個服務裡面都保存密鑰,那麼安全性將會大打折扣,要知道,密鑰一旦洩露,任何人都可以隨意偽造JWT 。
第三個參數:是簽署的選項SignOptions
,介面的簽章:
export interface SignOptions { algorithm?: Algorithm | undefined; keyid?: string | undefined; expiresIn?: string | number | undefined; /** expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js). Eg: 60, "2 days", "10h", "7d" */ notBefore?: string | number | undefined; audience?: string | string[] | undefined; subject?: string | undefined; issuer?: string | undefined; jwtid?: string | undefined; mutatePayload?: boolean | undefined; noTimestamp?: boolean | undefined; header?: JwtHeader | undefined; encoding?: string | undefined; }
這裡我們用的是expiresIn
字段,指定了時效時間,使用方法參考這篇文件;
第四個參數是一個回調,回呼的第二個參數就是我們透過簽章產生的token
,最後將這個token
回傳給前端,以便儲存到前端本地每次請求是帶上對服務端進行驗證。
接下來,我們來驗證一下這個介面: 我是在vscode安裝的REST Client插件,之後在根目錄創建一個request.http
的文件,文件內寫上請求的資訊:
POST http://localhost:3000/login content-type: application/json { "username": "username", "password": "password" }
之後在命令列執行npm run start
指令啟動服務,之後在requset.http
檔案上方點選Send Request
按鈕,發送請求:
請求成功後,會看到這樣的回應封包:
token
欄位就是我們JWT產生的token
;
下面來驗證一下這個token
是否有效,我們在寫一個登入後的介面:
app.get("/afterlogin", (req, res) => { const { headers } = req; const token = headers["authorization"].split(" ")[1]; // 將token放在header的authorization欄位中 jwt.verify(token, jwtKey, (err, payload) => { if (err) return res.sendStatus(403); res.json({ message: "認證成功", payload }); }); });
這段程式碼中,透過取得請求頭中的authorization
欄位中的token
進行取得先前透過JWT產生的token
。 之後透過呼叫jwt.verify
校驗方法校驗這個token
是否有效,這個方法分別有三個參數:
// 有四個介面簽名,可以自行查詢文件export function verify( token: string, // 需要檢驗的token secretOrPublicKey: Secret | GetPublicKeyOrSecret, // 定義在伺服器的簽章密碼金 callback?: VerifyCallback<JwtPayload | string>, // 取得校驗資訊結果的回呼): void;
接下來我們把剛才回應的token
複製到請求頭中:
### GET http://localhost:3000/afterlogin authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwiaWF0IjoxNjUyNzg5NzA3LCJle N58Hk_XEP5y9GM9A8jBbY
前面的Bearer認證, 是http協定中的標準認證方式
同樣點擊Send Request
當看到下面圖片的回應,就意味著回應成功:
其實以上就是JWT的一些簡單的用法,接下來再說一下JWT本身存在的優缺點.
JWT佔用的儲存空間其實並不小,如果我們需要簽名做過多的信息,那麼token很可能會超出cookie的長度限制,例如比較一下這兩張圖片:
很明顯,隨著payload的資訊量增大,token的長度也會增加;
安全性,其實如果token
的佔用空間過大, Cookie
最大存儲空間只有4kb前端可以存儲在localStorage
之類的本地存儲,但是會帶來一個問題,如果不是放在cookie的話,安全性就會大打折扣,就會有透過js腳本獲得到的風險,就意味著任何hacker都可以拿著它做任何事;
不靈活的時效性,其實JWT的某方面意義在於用戶token
不需要持久化存儲,而是採用伺服器校驗的方式對token
進行有效校驗,剛才看到了,簽名也是把到期時間一併簽名的,如果改變到期時間token
就會被篡改,由於沒有存儲和手動更改時效的方法,所以很難立刻將這個token
刪掉,如果用戶重複登陸兩次,生成兩個token
,那麼原則上兩個token
都是有效的;
以上主要講了幾點:
JWT的原理,主要是透過伺服器的私鑰對JSON的簽章所產生的token
進行會話;
也介紹了JWT內部數據的組成,是由Header用來指定簽名演算法和類型的,payload來傳輸JSON數據,Signature來對數據進行簽名演算法,放置篡改;
具體介紹了一下如何透過nodejs使用JWT,透過sign
方法進行資料簽名, verify
方法進行簽名驗證;
也介紹了一些JWT的不足:
一個是儲存空間隨著簽章資料量的增加而增加;
再有就是安全性,如果因為儲存空間過大將無法儲存在安全等級相對較高的Cookie
中,導致腳本可以隨意取得;
再有就是時效性,無法靈活的控制token
的時效性;
這個是上面nodejs的demo源碼,可供參考;