在前端开发中,实现无感刷新 token 通常是为了保持用户的登录状态,并确保其访问权限的有效性。下面将介绍如何实现无感刷新 token 的原理以及给出一个示例。
前端如何无感刷新token 实现原理
无感刷新 token 的实现通常基于以下几个步骤:
- Token 有效期:首先,服务器会为每个 token 设置一个有效期,例如 30 分钟或 1 小时。在这个有效期内,用户可以使用 token 访问受保护的资源。
- 定期检查:前端应用会在用户活动期间定期检查 token 的有效期。通常通过轮询或心跳机制实现,即在一定时间间隔后发送请求到服务器,检查 token 是否仍然有效。
- 刷新 Token :如果服务器返回 token 已失效的信息,前端应用会立即发起一个新的请求到认证服务器,使用refreshToken这个token(后端一般会返回一个长token和一个短token,长短指的是过期时间,refreshToken作为长token存在localstorage中用来向后端携带这个token去请求短token,accessToken作为短token也是存在localstorage中用这个token去请求受保护的请求放到请求头headers中)来获取新的访问 token。
- 无缝切换:在获取到新 token 后,前端应用会立即更新本地存储的 token,并使用新 token 继续之前的操作,从而实现了无缝的用户体验。
示例
下面是一个简单的示例,展示了如何在前端实现无感刷新 token:
- 设置 Token 有效期:假设服务器为每个 token 设置了 30 分钟的有效期。
- 定期检查:前端应用每 5 分钟向服务器发送一个请求,检查当前 token 是否仍然有效。
js
// 使用 setInterval 定时发送心跳请求
setInterval(() => {
checkTokenValidity();
}, 5 * 60 * 1000); // 5 分钟
- 检查 Token 有效性:前端应用发送心跳请求到服务器,服务器返回 token 是否有效的信息。
js
async function checkTokenValidity() {
try {
const response = await fetch('/api/heartbeat', {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`
}
});
if (!response.ok) {
// Token 失效,需要刷新
refreshToken();
}
} catch (error) {
// 处理错误
console.error('Error checking token validity:', error);
}
}
- 刷新 Token:如果 token 失效,前端应用会发送一个请求到认证服务器,获取新的访问 token。
js
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();
// 更新本地存储的 token
localStorage.setItem('accessToken', data.accessToken);
} else {
// 处理刷新 token 失败的情况,例如提示用户重新登录
console.error('Failed to refresh token:', response.status);
}
} catch (error) {
// 处理错误
console.error('Error refreshing token:', error);
}
}
在这个示例中,前端应用通过定期检查 token 的有效性,并在 token 失效时无缝刷新 token,从而保持了用户的登录状态,并提供了无缝的用户体验。需要注意的是,为了安全起见,刷新 token 也应该有一定的有效期,并在过期后要求用户重新登录。
token还没有刷新完成时,发送了其他请求需要如何处理
如果刷新的 token 还没有返回,但是你已经使用旧的 token 发送了其他三个请求到服务端,那么可能会遇到以下几种情况:
请求被拒绝:如果服务端检查到 token 已经过期,它可能会返回一个 401 Unauthorized 或者类似的错误状态码。在这种情况下,你需要捕获这些错误,并在捕获到错误后尝试使用新的 token 重新发送这些请求。
为了处理这些情况,你可以采取以下策略:
- 错误处理:确保你的应用能够捕获和处理 401 Unauthorized 或其他相关错误。当捕获到这些错误时,你可以尝试使用新的 token 重新发送请求。
- 请求重试机制:实现一个请求重试机制,当请求失败时,可以自动或手动触发重试,并使用新的 token。你可以使用指数退避策略来避免频繁重试对服务端造成压力。
- 状态管理:在 token 刷新期间,你可以将应用的状态设置为"正在刷新 token",并禁止发送新的请求,直到获取到新的 token。这样可以避免使用无效的 token 发送更多请求。
- 队列请求:在 token 过期后,而不是立即发送请求,你可以将请求放入队列中,等待新的 token 获取后再依次发送。这可以通过使用 Promise.all 或者其他异步处理技术来实现。
- 前端提示 :在 token 过期后,你可以在前端显示一个提示,告知用户正在刷新 token,并可能需要等待一段时间或者重新登录。
下面是一个简化的示例代码,
展示了如何在捕获到 401 错误后使用新 token 重新发送请求:
js
// 假设这是你的 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) {
// 尝试使用新 token 重新发送请求
return apiRequest(url, newToken);
}
throw error;
}
}
// 假设这是你的 token 刷新函数
let newToken = null; // 存储新 token 的变量
async function refreshToken() {
try {
const response = await fetch('/api/auth/refresh', {
// ... 发送刷新请求的代码 ...
});
if (response.ok) {
const data = await response.json();
newToken = data.token;
}
} catch (error) {
// 处理错误
}
}
// 当 token 过期时调用此函数
async function handleTokenExpiration() {
try {
// 尝试刷新 token
await refreshToken();
// 假设这是之前因为 token 过期而失败的请求数组
const failedRequests = [/* ... */];
// 使用新 token 重新发送请求
for (const request of failedRequests) {
try {
const response = await apiRequest(request.url, newToken);
// 处理响应或更新应用状态
} catch (error) {
// 处理重新发送请求时的错误
}
}
} catch (error) {
// 处理刷新 token 或重新发送请求时的错误
}
}
在这个示例中,apiRequest
函数会在遇到 401 错误时检查是否有新的 token 可用,并尝试使用新 token 重新发送请求。handleTokenExpiration
函数则负责在 token 过期时刷新 token,并重新发送之前失败的请求。
队列请求
队列请求的实现主要是利用队列这种数据结构来管理待处理的请求。当某个请求因为某些原因(比如token过期)需要延迟处理时,我们可以将这个请求放入队列中,等待条件满足(比如获取到新的token)后再从队列中取出请求进行处理。
下面是一个简单的例子来说明队列请求的实现:
1. 创建请求队列
首先,我们需要一个队列来存储待处理的请求。这个队列可以是内存中的一个数组、链表,或者使用一些现成的队列库(如JavaScript中的Array.prototype.queue
或queue-promise
库)。
js
let requestQueue = []; // 使用数组作为简单的队列实现
2. 入队操作
当需要延迟处理一个请求时,我们将这个请求添加到队列的末尾。
js
function enqueueRequest(request) {
requestQueue.push(request);
}
3. 出队操作
当条件满足(比如获取到新的token)时,我们从队列的头部取出一个请求进行处理,并重复这个过程直到队列为空。
js
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等。
js
async function processRequest(request) {
// 这里是处理请求的逻辑,比如发送HTTP请求
const response = await fetch(request.url, {
headers: {
Authorization: `Bearer ${newToken}` // 使用新的token
}
});
return response.json(); // 返回处理结果
}
5. 使用示例
当token过期时,我们可以将需要延迟处理的请求加入队列,然后尝试刷新token。一旦获取到新的token,我们从队列中取出请求并处理它们。
js
// 假设这是因为token过期而需要延迟处理的请求
const delayedRequest = { url: '/api/data' };
// 将请求加入队列
enqueueRequest(delayedRequest);
// 尝试刷新token
await refreshToken();
// 处理队列中的请求
await dequeueAndProcessRequests();
注意事项
- 队列大小管理:如果请求队列变得非常大,可能会消耗大量内存。你可能需要实现一些策略来限制队列的大小,比如设置最大长度、淘汰最旧的请求等。
- 错误处理:在处理请求和响应时,务必进行适当的错误处理,以避免应用崩溃或产生不可预期的行为。
- 并发控制:如果你的应用需要处理大量并发请求,你可能需要实现一些机制来控制并发数,比如使用Promise.all限制同时处理的请求数量。