關於無感刷新 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