什么是JWT?本篇文章帶大家了解一下JWT,介紹一下JWT在node中的應(yīng)用,以及JWT的優(yōu)缺點(diǎn),希望對大家有所幫助!

什么是JWT
JWT也就是JSON Web Token的縮寫,也就是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境中一種認(rèn)證解決方案,在傳統(tǒng)的認(rèn)證機(jī)制中,無非是一下幾個(gè)步驟:
1. 用戶將賬號(hào)密碼發(fā)送到服務(wù)器;
2. 服務(wù)器通過驗(yàn)證賬號(hào)密碼后,會(huì)在當(dāng)前session中保存一些用戶相關(guān)的信息,用戶角色或者過期時(shí)間等等;
3. 服務(wù)器給用戶一個(gè)session_id, 寫入用戶的Cookie或者客戶端自行保存在本地;
4. 用戶每次請求服務(wù),都需要帶上這個(gè)session_id,或許會(huì)通過Cookie,或者其他的方式;
5. 服務(wù)器接收到后,回去數(shù)據(jù)庫查詢當(dāng)前的session_id,校驗(yàn)該用戶是否有權(quán)限;
這種模式有一種優(yōu)勢在于,服務(wù)器隨時(shí)可以終止用戶的權(quán)限,可以去數(shù)據(jù)庫修改或者刪除當(dāng)前用戶的session信息。但是也有一點(diǎn)不好的,就是如果是服務(wù)器集群的話,所有的機(jī)器就需要共享這些session信息,確保每臺(tái)服務(wù)器都能夠獲取到相同的session存儲(chǔ)信息。雖然可以解決這些問題,但是工程量巨大。
JWT方案的優(yōu)勢呢,就是不保存這些信息,token數(shù)據(jù)保存在客戶端,每次接受請求時(shí),只需要校驗(yàn)就好。
JWT的原理
簡單說一下JWT的原理,其實(shí)就是客戶端發(fā)送請求認(rèn)證的時(shí)候,服務(wù)器在認(rèn)證用戶之后,會(huì)生成一個(gè)JSON對象,大概包括“你是誰,你是干嘛的等等,到期時(shí)間”這些信息,重要的是一定要有到期時(shí)間;大致格式為:
{
username: "賊煩字符串er",
role: "世代碼農(nóng)",
endTime: "2022年5月20日"
}但是不會(huì)用這么膚淺的方式傳給你,它會(huì)通過制定的簽名算法和你提交的payload的一些信息進(jìn)行可逆的簽名算法進(jìn)行簽名后傳輸,大致的格式我用一張圖片表示:

由圖片可以看出,返回的信息大致分為三部分,左側(cè)為簽名之后的結(jié)果,也就是返回給客戶端的結(jié)果,右側(cè)也是就Decoded的源碼了,三部分由“點(diǎn)”隔開,分別由紅、紫、青三種顏色一一對應(yīng):
第一個(gè)紅色部分是Header,Header中主要是指定了的方式,圖中的簽名算法(默認(rèn)HS256)就是帶有 SHA-256 的 HMAC 是一種對稱算法, 雙方之間僅共享一個(gè)密鑰,typ字段標(biāo)識(shí)為JWT類型;
第二個(gè)紫色部分payload,就是一個(gè)JSON對象,也就是實(shí)際要傳輸?shù)臄?shù)據(jù),官方有七個(gè)字段可以使用:
iss (issuer):簽發(fā)人
exp (expiration time):過期時(shí)間
sub (subject):主題
aud (audience):受眾
nbf (Not Before):生效時(shí)間
iat (Issued At):簽發(fā)時(shí)間
jti (JWT ID):編號(hào)
除了這些字段,你還可以搞一些自定義的字段,由于JWT默認(rèn)是不加密的,所以在使用的時(shí)候盡量注意不要使用一些敏感數(shù)據(jù)。
第三部分就是
Signature簽名,這一部分,是由你自己指定且只有服務(wù)器存在的秘鑰,然后使用頭部指定的算法通過下面的簽名方法進(jìn)行簽名。
JWT的簡單使用
下面我們來感受一下具體的使用:
第一步:我們需要搭建一個(gè)nodejs的項(xiàng)目;通過npm init -y初始化一個(gè)項(xiàng)目;之后我們需要安裝依賴,分別按狀express、jsonwebtoken和nodemon三個(gè)依賴:
npm i express jsonwebtoken nodemon
之后在package.json中的scripts字段中添加nodemon app.js命令:
"scripts": {
"start": "nodemon app.js"
},第二步:初始化一下node應(yīng)用,在根目錄下創(chuàng)建app.js文件;
// app.js
const express = require("express");
const app = express();
app.use(express.json());
app.listen(3000, () => {
console.log(3000 + " listening..."); // 監(jiān)聽3000端口
});第三步:引入jsonwebtoken依賴,并且創(chuàng)建接口和服務(wù)器的私鑰;
// app.js
//...
const jwt = require("jsonwebtoken");
const jwtKey = "~!@#$%^&*()+,";
// ...這里面的jwtKey是我們自定義保存僅限保存在服務(wù)器中的私鑰,之后我們開始寫一個(gè) /login 接口,用來登錄,并且創(chuàng)建本地的模擬數(shù)據(jù)庫用來校驗(yàn),并通過jwt.sign方法進(jìn)行校驗(yàn)簽名:
// 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,
});
}
);
}
});上面代碼中我們創(chuàng)建了database變量來模擬創(chuàng)建了本地的賬號(hào)密碼數(shù)據(jù)庫,用來校驗(yàn)登陸;接下來建立了一個(gè)/login的post接口,在校驗(yàn)賬號(hào)密碼完全匹配之后,我們通過jsonwebtoken包導(dǎo)入的jwt對象下的人sign方法進(jìn)行簽名,這個(gè)方法有三種接口簽名:
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;
這里用到了函數(shù)重載的方式實(shí)現(xiàn)接口,我們這里將實(shí)現(xiàn)最后一個(gè)接口簽名,第一個(gè)參數(shù)可以是一個(gè)自定義的對象類型,也可以是一個(gè)Buffer類型,還可以直接是一個(gè)string類型,我們的源碼使用了object類型,自定義了一些字段,因?yàn)閖wt在進(jìn)行簽名是也會(huì)對這些數(shù)據(jù)一并進(jìn)行簽名,但是值得注意的是,這里盡量不要使用敏感數(shù)據(jù),因?yàn)镴WT默認(rèn)是不加密的,它的核心就是簽名,保證數(shù)據(jù)未被篡改,而檢查簽名的過程就叫做驗(yàn)證。
當(dāng)然你也可以對原始Token進(jìn)行加密后傳輸;
第二個(gè)參數(shù):是我們保存在服務(wù)器用來簽名的秘鑰,通常在客戶端-服務(wù)端模式中,JWS 使用 JWA 提供的 HS256 算法加上一個(gè)密鑰即可,這種方式嚴(yán)格依賴密鑰,但在分布式場景,可能多個(gè)服務(wù)都需要驗(yàn)證JWT,若要在每個(gè)服務(wù)里面都保存密鑰,那么安全性將會(huì)大打折扣,要知道,密鑰一旦泄露,任何人都可以隨意偽造JWT。
第三個(gè)參數(shù):是簽名的選項(xiàng)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字段,指定了時(shí)效時(shí)間,使用方法參考這個(gè)文檔;
第四個(gè)參數(shù)是一個(gè)回調(diào),回調(diào)的第二個(gè)參數(shù)就是我們通過簽名生成的token,最后將這個(gè)token返回給前端,以便存儲(chǔ)到前端本地每次請求是帶上到服務(wù)端進(jìn)行驗(yàn)證。
接下來,我們來驗(yàn)證一下這個(gè)接口: 我是在vscode安裝的REST Client插件,之后在根目錄創(chuàng)建一個(gè)request.http的文件,文件內(nèi)寫上請求的信息:
POST http://localhost:3000/login
content-type: application/json
{
"username": "username",
"password": "password"
}之后在命令行執(zhí)行npm run start命令啟動(dòng)服務(wù),之后在requset.http文件上方點(diǎn)擊Send Request按鈕,發(fā)送請求:

請求成功后,會(huì)看到這樣的響應(yīng)報(bào)文:

token字段就是我們JWT生成的token;
下面來驗(yàn)證一下這個(gè)token是否有效,我們在寫一個(gè)登錄過后的接口:
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: "認(rèn)證成功", payload });
});
});這段代碼中,通過獲取請求頭中的authorization字段中的token進(jìn)行獲取之前通過JWT生成的token。 之后通過調(diào)用jwt.verify校驗(yàn)方法校驗(yàn)這個(gè)token是否有效,這個(gè)方法分別有三個(gè)參數(shù):
// 有四個(gè)接口簽名,可以自行查文檔 export function verify( token: string, // 需要檢驗(yàn)的token secretOrPublicKey: Secret | GetPublicKeyOrSecret, // 定義在服務(wù)器的簽名秘鑰 callback?: VerifyCallback<JwtPayload | string>, // 獲取校驗(yàn)信息結(jié)果的回調(diào) ): void;
接下來我們把剛才響應(yīng)的token復(fù)制到請求頭中:
### GET http://localhost:3000/afterlogin authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXJuYW1lIiwiaWF0IjoxNjUyNzg5NzA3LCJleHAiOjE2NTI3ODk3Mzd9.s9fk3YLhxTUcpUgCfIK4xQN58Hk_XEP5y9GM9A8jBbY
前面的Bearer認(rèn)證, 是http協(xié)議中的標(biāo)準(zhǔn)認(rèn)證方式
同樣點(diǎn)擊Send Request當(dāng)看到下面圖片的響應(yīng),就意味著響應(yīng)成功:

其實(shí)以上就是JWT的一些簡單的用法,接下來再說一下JWT本身存在的優(yōu)缺點(diǎn).
JWT的不足
JWT占用的存儲(chǔ)空間其實(shí)并不小,如果我們需要簽名做過多的信息,那么token很可能會(huì)超出cookie的長度限制,例如對比一下這兩張圖片:

很明顯,隨著payload的信息量增大,token的長度也會(huì)增加;
安全性,其實(shí)如果
token的占用空間過大,Cookie最大存儲(chǔ)空間只有4kb前端可以存儲(chǔ)在localStorage之類的本地存儲(chǔ),但是會(huì)帶來一個(gè)問題,如果不是放在cookie的話,安全性就會(huì)大打折扣,就會(huì)有通過js腳本獲取到的風(fēng)險(xiǎn),就意味著任何hacker都可以拿著它做任何事情;不靈活的時(shí)效性,其實(shí)JWT的某方面意義在于用戶
token不需要持久化存儲(chǔ),而是采用服務(wù)器校驗(yàn)的方式對token進(jìn)行有效校驗(yàn),剛才看到了,簽名也是把到期時(shí)間一并簽名的,如果改變到期時(shí)間token就會(huì)被篡改,由于沒有存儲(chǔ)和手動(dòng)更改時(shí)效的方法,所以很難立刻將這個(gè)token刪掉,如果用戶重復(fù)登陸兩次,生成兩個(gè)token,那么原則上兩個(gè)token都是有效的;
總結(jié)
以上主要講了幾點(diǎn):
JWT的原理,主要是通過服務(wù)器的私鑰對JSON的簽名產(chǎn)生的
token進(jìn)行會(huì)話;也介紹了JWT內(nèi)部數(shù)據(jù)的組成,是由Header用來指定簽名算法和類型的,payload來傳輸JSON數(shù)據(jù),Signature來對數(shù)據(jù)進(jìn)行簽名算法,放置篡改;
具體介紹了一下如何通過nodejs使用JWT,通過
sign方法進(jìn)行數(shù)據(jù)簽名,verify方法進(jìn)行簽名驗(yàn)證;
還介紹了一些JWT的不足:
一個(gè)是存儲(chǔ)空間隨著簽名數(shù)據(jù)量的增大而增加;
再有就是安全性,如果由于存儲(chǔ)空間過大將無法存儲(chǔ)在安全級(jí)別相對較高的
Cookie中,導(dǎo)致腳本可以隨意獲取;再有就是時(shí)效性,無法靈活的控制
token的時(shí)效性;
這個(gè)是上面nodejs的demo源碼,可供參考;
https://github.com/wangzi6224/jwt-usage-nodejs






