關(guān)于無感刷新 Token ,看這一篇就夠了?。?!
什么是JWT
JWT是全稱是JSON WEB TOKEN,是一個開放標(biāo)準(zhǔn),用于將各方數(shù)據(jù)信息作為JSON格式進行對象傳遞,可以對數(shù)據(jù)進行可選的數(shù)字加密,可使用RSA或ECDSA進行公鑰/私鑰簽名。
使用場景
JWT最常見的使用場景就是緩存當(dāng)前用戶登錄信息,當(dāng)用戶登錄成功之后,拿到JWT,之后用戶的每一個請求在請求頭攜帶上Authorization字段來辨別區(qū)分請求的用戶信息。且不需要額外的資源開銷。
相比傳統(tǒng)session的區(qū)別
比起傳統(tǒng)的session認證方案,為了讓服務(wù)器能識別是哪一個用戶發(fā)過來的請求,都需要在服務(wù)器上保存一份用戶的登錄信息(通常保存在內(nèi)存中),再與瀏覽器的cookie打交道。
安全方面 由于是使用cookie來識別用戶信息的,如果cookie被攔截,用戶會很容易受到跨站請求偽造的攻擊。
負載均衡 當(dāng)服務(wù)器A保存了用戶A的數(shù)據(jù)之后,在下一次用戶A服務(wù)器A時由于服務(wù)器A訪問量較大,被轉(zhuǎn)發(fā)到服務(wù)器B,此時服務(wù)器B沒有用戶A的數(shù)據(jù),會導(dǎo)致session失效。
內(nèi)存開銷 隨著時間推移,用戶的增長,服務(wù)器需要保存的用戶登錄信息也就越來越多的,會導(dǎo)致服務(wù)器開銷越來越大。
為什么說JWT不需要額外的開銷
JWT為三個部分組成,分別是Header,Payload,Signature,使用.符號分隔。
// 像這樣子
xxxxx.yyyyy.zzzzz
標(biāo)頭 header
標(biāo)頭是一個JSON對象,由兩個部分組成,分別是令牌是類型(JWT)和簽名算法(SHA256,RSA)
{
"alg": "HS256",
"typ": "JWT"
}
負荷 payload
負荷部分也是一個JSON對象,用于存放需要傳遞的數(shù)據(jù),例如用戶的信息
{
"username": "_island",
"age": 18
}
此外,JWT規(guī)定了7個可選官方字段(建議)
屬性 說明
iss JWT簽發(fā)人
exp JWT過期時間
sub JWT面向用戶
aud JWT接收方
nbf JWT生效時間
iat JWT簽發(fā)時間
jti JWT編號
簽章 signature
這一部分,是由前面兩個部分的簽名,防止數(shù)據(jù)被篡改。在服務(wù)器中指定一個密鑰,使用標(biāo)頭中指定的簽名算法,按照下面的公式生成這簽名數(shù)據(jù)
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
在拿到簽名數(shù)據(jù)之后,把這三個部分的數(shù)據(jù)拼接起來,每個部分中間使用.來分隔。這樣子我們就生成出一個了JWT數(shù)據(jù)了,接下來返回給客戶端儲存起來。而且客戶端在發(fā)起請求時,攜帶這個JWT在請求頭中的Authorization字段,服務(wù)器通過解密的方式即可識別出對應(yīng)的用戶信息。
JWT優(yōu)勢和弊端
優(yōu)勢
數(shù)據(jù)體積小,傳輸速度快
無需額外資源開銷來存放數(shù)據(jù)
支持跨域驗證使用
弊端
生成出來的Token無法撤銷,即使重置賬號密碼之前的Token也是可以使用的(需等待JWT過期)
無法確認用戶已經(jīng)簽發(fā)了多少個JWT
不支持refreshToken
關(guān)于refreshToken
refreshToken是Oauth2認證中的一個概念,和accessToken一起生成出來的。
當(dāng)用戶攜帶的這個accessToken過期時,用戶就需要在重新獲取新的accessToken,而refreshToken就用來重新獲取新的accessToken的憑證。
為什么要有refreshToken
當(dāng)你第一次接觸的時候,你有沒有一個這樣子的疑惑,為什么需要refreshToken這個東西,而不是服務(wù)器端給一個期限較長甚至永久性的accessToken呢?
抱著這個疑惑我在網(wǎng)上搜尋了一番,
其實這個accessToken的使用期限有點像我們生活中的入住酒店,當(dāng)我們在入住酒店時,會出示我們的身份證明來登記獲取房卡,此時房卡相當(dāng)于accessToken,可以訪問對應(yīng)的房間,當(dāng)你的房卡過期之后就無法再開啟房門了,此時就需要再到前臺更新一下房卡,才能正常進入,這個過程也就相當(dāng)于refreshToken。
accessToken使用率相比refreshToken頻繁很多,如果按上面所說如果accessToken給定一個較長的有效時間,就會出現(xiàn)不可控的權(quán)限泄露風(fēng)險。
使用refreshToken可以提高安全性
用戶在訪問網(wǎng)站時,accessToken被盜取了,此時攻擊者就可以拿這個accessToke訪問權(quán)限以內(nèi)的功能了。如果accessToken設(shè)置一個短暫的有效期2小時,攻擊者能使用被盜取的accessToken的時間最多也就2個小時,除非再通過refreshToken刷新accessToken才能正常訪問。
設(shè)置accessToken有效期是永久的,用戶在更改密碼之后,之前的accessToken也是有效的
總體來說有了refreshToken可以降低accessToken被盜的風(fēng)險
關(guān)于JWT無感刷新TOKEN方案(結(jié)合axios)
業(yè)務(wù)需求
在用戶登錄應(yīng)用后,服務(wù)器會返回一組數(shù)據(jù),其中就包含了accessToken和refreshToken,每個accessToken都有一個固定的有效期,如果攜帶一個過期的token向服務(wù)器請求時,服務(wù)器會返回401的狀態(tài)碼來告訴用戶此token過期了,此時就需要用到登錄時返回的refreshToken調(diào)用刷新Token的接口(Refresh)來更新下新的token再發(fā)送請求即可。
話不多說,先上代碼
工具
axios作為最熱門的http請求庫之一,我們本篇文章就借助它的錯誤響應(yīng)攔截器來實現(xiàn)token無感刷新功能。
具體實現(xiàn)
本次基于axios-bz[1]代碼片段封裝響應(yīng)攔截器 可直接配置到你的項目中使用 ?? ??
利用interceptors.response,在業(yè)務(wù)代碼獲取到接口數(shù)據(jù)之前進行狀態(tài)碼401判斷當(dāng)前攜帶的accessToken是否失效。下面是關(guān)于interceptors.response中異常階段處理內(nèi)容。當(dāng)響應(yīng)碼為401時,響應(yīng)攔截器會走中第二個回調(diào)函數(shù)onRejected
下面代碼分段可能會讓大家閱讀起來不是很順暢,我直接把整份代碼貼在下面,且每一段代碼之間都添加了對應(yīng)的注釋
// 最大重發(fā)次數(shù)
const MAX_ERROR_COUNT = 5;
// 當(dāng)前重發(fā)次數(shù)
let currentCount = 0;
// 緩存請求隊列
const queue: ((t: string) => any)[] = [];
// 當(dāng)前是否刷新狀態(tài)
let isRefresh = false;
export default async (error: AxiosError<ResponseDataType>) => {
const statusCode = error.response?.status;
const clearAuth = () => {
console.log('身份過期,請重新登錄');
window.location.replace('/login');
// 清空數(shù)據(jù)
sessionStorage.clear();
return Promise.reject(error);
};
// 為了節(jié)省多余的代碼,這里僅展示處理狀態(tài)碼為401的情況
if (statusCode === 401) {
// accessToken失效
// 判斷本地是否有緩存有refreshToken
const refreshToken = sessionStorage.get('refresh') ?? null;
if (!refreshToken) {
clearAuth();
}
// 提取請求的配置
const { config } = error;
// 判斷是否refresh失敗且狀態(tài)碼401,再次進入錯誤攔截器
if (config.url?.includes('refresh')) {
clearAuth();
}
// 判斷當(dāng)前是否為刷新狀態(tài)中(防止多個請求導(dǎo)致多次調(diào)refresh接口)
if (!isRefresh) {
// 設(shè)置當(dāng)前狀態(tài)為刷新中
isRefresh = true;
// 如果重發(fā)次數(shù)超過,直接退出登錄
if (currentCount > MAX_ERROR_COUNT) {
clearAuth();
}
// 增加重試次數(shù)
currentCount += 1;
try {
const {
data: { access },
} = await UserAuthApi.refreshToken(refreshToken);
// 請求成功,緩存新的accessToken
sessionStorage.set('token', access);
// 重置重發(fā)次數(shù)
currentCount = 0;
// 遍歷隊列,重新發(fā)起請求
queue.forEach((cb) => cb(access));
// 返回請求數(shù)據(jù)
return ApiInstance.request(error.config);
} catch {
// 刷新token失敗,直接退出登錄
console.log('請重新登錄');
sessionStorage.clear();
window.location.replace('/login');
return Promise.reject(error);
} finally {
// 重置狀態(tài)
isRefresh = false;
}
} else {
// 當(dāng)前正在嘗試刷新token,先返回一個promise阻塞請求并推進請求列表中
return new Promise((resolve) => {
// 緩存網(wǎng)絡(luò)請求,等token刷新后直接執(zhí)行
queue.push((newToken: string) => {
Reflect.set(config.headers!, 'authorization', newToken);
// @ts-ignore
resolve(ApiInstance.request<ResponseDataType<any>>(config));
});
});
}
}
return Promise.reject(error);
};
抽離代碼
把上面關(guān)于調(diào)用刷新token的代碼抽離成一個refreshToken函數(shù),單獨處理這一情況,這樣子做有利于提高代碼的可讀性和維護性,且讓看上去代碼不是很臃腫
// refreshToken.ts
export default async function refreshToken(error: AxiosError<ResponseDataType>) {
/*
將上面 if (statusCode === 401) 中的代碼貼進來即可,這里就不重復(fù)啦
代碼倉庫地址: https://github.com/QC2168/axios-bz/blob/main/Interceptors/hooks/refreshToken.ts
*/
}
經(jīng)過上面的邏輯抽離,現(xiàn)在看下攔截器中的代碼就很簡潔了,后續(xù)如果要調(diào)整相關(guān)邏輯直接在refreshToken.ts文件中調(diào)整即可。
import refreshToken from './refreshToken.ts'
export default async (error: AxiosError<ResponseDataType>) => {
const statusCode = error.response?.status;
// 為了節(jié)省多余的代碼,這里僅展示處理狀態(tài)碼為401的情況
if (statusCode === 401) {
refreshToken()
}
return Promise.reject(error);
};
作者:_island https://juejin.cn/post/7170278285274775560
參考資料
[1]
axios-bz: https://github.com/QC2168/axios-bz
作者:_island
歡迎關(guān)注微信公眾號 :前端開發(fā)愛好者
添加好友備注【進階學(xué)習(xí)】拉你進技術(shù)交流群