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