在前端开发中,经常会遇到 Token续约 的问题。对 Token 实现无缝刷新从而维护用户的登录状态无论是在开发时,还是在 面试时都是至关重要的。所以说咱们今天就来看看 Token 的无缝刷新问题。
一、前端无缝刷新令牌的原理
1、令牌过期
服务器为每个令牌设置一个过期时间,通常是30分钟或1小时。在这段时间内,用户可以使用令牌来访问受保护的资源。
2、定期检查
前端应用程序在用户活动期间定期检查令牌的有效性。通常通过轮询或心跳机制实现,即定期向服务器发送请求,以验证令牌的有效性。
3、令牌刷新
如果服务器指示令牌已过期,前端应用程序立即使用refreshToken向身份验证服务器发送新请求,以获取新的访问令牌。无缝过渡:在接收到新令牌后,前端应用程序会更新本地存储的令牌,并继续以前的操作,完全不受影响。
4、示例
下面是一个简单的示例,演示如何在前端实现无缝刷新令牌:
令牌过期:假设服务器为每个令牌设置了30分钟的过期时间。
定期检查:前端应用程序每5分钟向服务器发送一次请求,检查当前令牌的有效性。
// 使用setInterval定期发送心跳请求
setInterval(() => {
checkTokenValidity();
}, 5 * 60 * 1000); // 5 分钟
检查令牌有效性:前端应用程序向服务器发送心跳请求以验证令牌的有效性。
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);
}
}
令牌刷新:如果令牌过期了,前端应用程序会向身份验证服务器发送请求,以获取一个新的访问令牌。
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);
}
}
在这个例子中,前端应用程序通过定期检查令牌的有效性并在需要时发起刷新,从而无缝刷新令牌,确保了用户体验的流畅性。需要注意的是,出于安全原因,令牌刷新也应该有一个过期时间,并且在过期后用户可能需要重新登录。
二、在令牌刷新过程中处理请求
如果在令牌刷新进行中并且新令牌尚未到达时发送额外的请求,可能会遇到以下这种情况:
如果服务器检测到一个过期的令牌,可能会返回401未经授权或类似的错误状态。在这种情况下,你需要捕获这些错误并在捕获到错误时尝试使用新令牌重新发送请求。
为处理这些情况,可以采取以下策略:
1、错误处理
确保你的应用能够捕获和处理 401 Unauthorized 或相关的错误。捕获这些错误后,尝试使用新令牌重新发送请求。
2、请求重试机制
实现一个请求重试机制,自动或手动重新尝试使用新令牌的失败请求。你可以使用指数退避策略来避免频繁的重试和减少服务器负载。
3、状态管理
在令牌刷新期间,将应用程序状态设置为“刷新令牌”,并防止新请求,直到获得新令牌为止。这可以防止使用无效令牌发送更多的请求。
4、请求排队
在令牌过期后,将请求排队,等待获得新令牌后再逐个发送请求。可以使用Promise.all或其他异步处理技术来实现这一点。
5、前端通知
在令牌过期后,在前端显示通知,告知用户有关正在进行的令牌刷新过程以及需要等待或有可能重新登录的需要。
以下是一个简化的代码示例,演示了在捕获到401错误后如何使用新令牌重新发送请求。
假设这是你的API请求函数:
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) {
// 尝试使用新令牌重新发送请求
return apiRequest(url, newToken);
}
throw error;
}
}
// 假设这是你的 token 刷新功能
let newToken = null; // 用于存储新令牌的变量
async function refreshToken() {
try {
const response = await fetch('/api/auth/refresh', {
// ... 发送刷新请求的代码 ...
});
if (response.ok) {
const data = await response.json();
newToken = data.token;
}
} catch (error) {
// 处理错误
}
}
// 当令牌到期时调用此函数
async function handleTokenExpiration() {
try {
// 尝试刷新令牌
await refreshToken();
// 假设这是之前因令牌过期而失败的请求的数组
const failedRequests = [/* ... */];
// 使用新令牌重新发送请求
for (const request of failedRequests) {
try {
const response = await apiRequest(request.url, newToken);
// 处理响应或更新应用程序状态
} catch (error) {
// 处理请求重新发送期间的错误
}
}
} catch (error) {
// 处理令牌刷新或请求重新发送期间的错误
}
}
在这个示例中,apiRequest函数检查 401 错误,并在有新令牌时尝试重新发送请求。handleTokenExpiration函数通过刷新令牌并使用新令牌重新发送先前失败的请求来处理令牌过期问题。
三、排队请求
请求排队的实现依赖于使用队列数据结构来管理待处理请求。当由于某些条件(例如,令牌过期)需要延迟处理请求时,请求将被添加到队列中,等待条件满足(例如,获取新令牌)后再进行处理。
下面是一个简单的示例,演示了如何实现请求排队:
1、创建一个请求队列
首先,你需要一个队列来存储待处理的请求。这个队列可以是一个数组,内存中的链表,或者使用现有的队列库(例如JavaScript中的Array.prototype.queue或queue-promise)。
let requestQueue = []; // 使用数组作为简单的队列实现
2、排队操作
当请求需要延迟时,请将其添加到队列的末尾。
function enqueueRequest(request) {
requestQueue.push(request);
}
3、出队操作
当满足条件时(例如,获取新令牌),将请求从队列前端出队进行处理,并重复直到队列为空。
async function dequeueAndProcessRequests() {
while (requestQueue.length > 0) {
const request = requestQueue.shift(); // 从队列前面撤回请求
try {
const response = await processRequest(request); // 处理该请求
// 处理响应或更新应用程序状态
} catch (error) {
// 处理请求处理期间的错误
console.error('Error processing request:', error);
}
}
4、处理请求
该 processRequest 函数处理实际的请求处理逻辑,例如发送 HTTP 请求或更新 UI。
async function processRequest(request) {
// 实际请求处理逻辑,例如发送HTTP请求
const response = await fetch(request.url, {
headers: {
Authorization: `Bearer ${newToken}` // 使用新的 token
}
});
return response.json(); // 返回处理结果
}
5、使用示例
当令牌过期时,将需要延迟的请求排队,然后尝试刷新令牌。获得新令牌后,将请求从队列中出列并处理它们。
// 假设这是由于令牌过期而需要延迟的请求
const delayedRequest = { url: '/api/data' };
// 将请求排队
enqueueRequest(delayedRequest);
// 尝试刷新令牌
await refreshToken();
// 处理队列中的请求
await dequeueAndProcessRequests();