前端實現(xiàn)無縫刷新 Token
在前端開發(fā)中,經(jīng)常會遇到 Token續(xù)約 的問題。對 Token 實現(xiàn)無縫刷新從而維護(hù)用戶的登錄狀態(tài)無論是在開發(fā)時,還是在 面試時都是至關(guān)重要的。所以說咱們今天就來看看 Token 的無縫刷新問題。
一、前端無縫刷新令牌的原理
1、令牌過期
服務(wù)器為每個令牌設(shè)置一個過期時間,通常是30分鐘或1小時。在這段時間內(nèi),用戶可以使用令牌來訪問受保護(hù)的資源。
2、定期檢查
前端應(yīng)用程序在用戶活動期間定期檢查令牌的有效性。通常通過輪詢或心跳機(jī)制實現(xiàn),即定期向服務(wù)器發(fā)送請求,以驗證令牌的有效性。
3、令牌刷新
如果服務(wù)器指示令牌已過期,前端應(yīng)用程序立即使用refreshToken向身份驗證服務(wù)器發(fā)送新請求,以獲取新的訪問令牌。無縫過渡:在接收到新令牌后,前端應(yīng)用程序會更新本地存儲的令牌,并繼續(xù)以前的操作,完全不受影響。
4、示例
下面是一個簡單的示例,演示如何在前端實現(xiàn)無縫刷新令牌:
令牌過期:假設(shè)服務(wù)器為每個令牌設(shè)置了30分鐘的過期時間。
定期檢查:前端應(yīng)用程序每5分鐘向服務(wù)器發(fā)送一次請求,檢查當(dāng)前令牌的有效性。
// 使用setInterval定期發(fā)送心跳請求
setInterval(() => {
checkTokenValidity();
}, 5 * 60 * 1000); // 5 分鐘
檢查令牌有效性:前端應(yīng)用程序向服務(wù)器發(fā)送心跳請求以驗證令牌的有效性。
async function checkTokenValidity() {
try {
const response = await fetch('/api/heartbeat', {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`
}
});
if (!response.ok) {
// Token expired, initiate token refresh
refreshToken();
}
} catch (error) {
// Handle errors
console.error('Error checking token validity:', error);
}
}
令牌刷新:如果令牌過期了,前端應(yīng)用程序會向身份驗證服務(wù)器發(fā)送請求,以獲取一個新的訪問令牌。
async function refreshToken() {
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
refreshToken: localStorage.getItem('refreshToken')
})
});
if (response.ok) {
const data = await response.json();
// 更新本地存儲的令牌
localStorage.setItem('accessToken', data.accessToken);
} else {
// 處理令牌刷新失敗
console.error('Failed to refresh token:', response.status);
}
} catch (error) {
// 處理錯誤
console.error('Error refreshing token:', error);
}
}
在這個例子中,前端應(yīng)用程序通過定期檢查令牌的有效性并在需要時發(fā)起刷新,從而無縫刷新令牌,確保了用戶體驗的流暢性。需要注意的是,出于安全原因,令牌刷新也應(yīng)該有一個過期時間,并且在過期后用戶可能需要重新登錄。
二、在令牌刷新過程中處理請求
如果在令牌刷新進(jìn)行中并且新令牌尚未到達(dá)時發(fā)送額外的請求,可能會遇到以下這種情況:
如果服務(wù)器檢測到一個過期的令牌,可能會返回401未經(jīng)授權(quán)或類似的錯誤狀態(tài)。在這種情況下,你需要捕獲這些錯誤并在捕獲到錯誤時嘗試使用新令牌重新發(fā)送請求。
為處理這些情況,可以采取以下策略:
1、錯誤處理
確保你的應(yīng)用能夠捕獲和處理 401 Unauthorized 或相關(guān)的錯誤。捕獲這些錯誤后,嘗試使用新令牌重新發(fā)送請求。
2、請求重試機(jī)制
實現(xiàn)一個請求重試機(jī)制,自動或手動重新嘗試使用新令牌的失敗請求。你可以使用指數(shù)退避策略來避免頻繁的重試和減少服務(wù)器負(fù)載。
3、狀態(tài)管理
在令牌刷新期間,將應(yīng)用程序狀態(tài)設(shè)置為“刷新令牌”,并防止新請求,直到獲得新令牌為止。這可以防止使用無效令牌發(fā)送更多的請求。
4、請求排隊
在令牌過期后,將請求排隊,等待獲得新令牌后再逐個發(fā)送請求??梢允褂肞romise.all或其他異步處理技術(shù)來實現(xiàn)這一點。
5、前端通知
在令牌過期后,在前端顯示通知,告知用戶有關(guān)正在進(jìn)行的令牌刷新過程以及需要等待或有可能重新登錄的需要。
以下是一個簡化的代碼示例,演示了在捕獲到401錯誤后如何使用新令牌重新發(fā)送請求。
假設(shè)這是你的API請求函數(shù):
async function apiRequest(url, token) {
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
} catch (error) {
if (error.message.includes('401') && newToken) {
// 嘗試使用新令牌重新發(fā)送請求
return apiRequest(url, newToken);
}
throw error;
}
}
// 假設(shè)這是你的 token 刷新功能
let newToken = null; // 用于存儲新令牌的變量
async function refreshToken() {
try {
const response = await fetch('/api/auth/refresh', {
// ... 發(fā)送刷新請求的代碼 ...
});
if (response.ok) {
const data = await response.json();
newToken = data.token;
}
} catch (error) {
// 處理錯誤
}
}
// 當(dāng)令牌到期時調(diào)用此函數(shù)
async function handleTokenExpiration() {
try {
// 嘗試刷新令牌
await refreshToken();
// 假設(shè)這是之前因令牌過期而失敗的請求的數(shù)組
const failedRequests = [/* ... */];
// 使用新令牌重新發(fā)送請求
for (const request of failedRequests) {
try {
const response = await apiRequest(request.url, newToken);
// 處理響應(yīng)或更新應(yīng)用程序狀態(tài)
} catch (error) {
// 處理請求重新發(fā)送期間的錯誤
}
}
} catch (error) {
// 處理令牌刷新或請求重新發(fā)送期間的錯誤
}
}
在這個示例中,apiRequest函數(shù)檢查 401 錯誤,并在有新令牌時嘗試重新發(fā)送請求。handleTokenExpiration函數(shù)通過刷新令牌并使用新令牌重新發(fā)送先前失敗的請求來處理令牌過期問題。
三、排隊請求
請求排隊的實現(xiàn)依賴于使用隊列數(shù)據(jù)結(jié)構(gòu)來管理待處理請求。當(dāng)由于某些條件(例如,令牌過期)需要延遲處理請求時,請求將被添加到隊列中,等待條件滿足(例如,獲取新令牌)后再進(jìn)行處理。
下面是一個簡單的示例,演示了如何實現(xiàn)請求排隊:
1、創(chuàng)建一個請求隊列
首先,你需要一個隊列來存儲待處理的請求。這個隊列可以是一個數(shù)組,內(nèi)存中的鏈表,或者使用現(xiàn)有的隊列庫(例如JavaScript中的Array.prototype.queue或queue-promise)。
let requestQueue = []; // 使用數(shù)組作為簡單的隊列實現(xiàn)
2、排隊操作
當(dāng)請求需要延遲時,請將其添加到隊列的末尾。
function enqueueRequest(request) {
requestQueue.push(request);
}
3、出隊操作
當(dāng)滿足條件時(例如,獲取新令牌),將請求從隊列前端出隊進(jìn)行處理,并重復(fù)直到隊列為空。
async function dequeueAndProcessRequests() {
while (requestQueue.length > 0) {
const request = requestQueue.shift(); // 從隊列前面撤回請求
try {
const response = await processRequest(request); // 處理該請求
// 處理響應(yīng)或更新應(yīng)用程序狀態(tài)
} catch (error) {
// 處理請求處理期間的錯誤
console.error('Error processing request:', error);
}
}
4、處理請求
該 processRequest 函數(shù)處理實際的請求處理邏輯,例如發(fā)送 HTTP 請求或更新 UI。
async function processRequest(request) {
// 實際請求處理邏輯,例如發(fā)送HTTP請求
const response = await fetch(request.url, {
headers: {
Authorization: `Bearer ${newToken}` // 使用新的 token
}
});
return response.json(); // 返回處理結(jié)果
}
5、使用示例
當(dāng)令牌過期時,將需要延遲的請求排隊,然后嘗試刷新令牌。獲得新令牌后,將請求從隊列中出列并處理它們。
// 假設(shè)這是由于令牌過期而需要延遲的請求
const delayedRequest = { url: '/api/data' };
// 將請求排隊
enqueueRequest(delayedRequest);
// 嘗試刷新令牌
await refreshToken();
// 處理隊列中的請求
await dequeueAndProcessRequests();