大家好,今天的分享由团队的 uncle13 老师提供。
在大型企业或复杂的应用生态系统中,单点登录是一种常见的认证方式。用户只需登录一次,就可以访问多个应用或服务。
在这些场景中,无感刷新 Token 可以确保用户在整个会话期间保持登录状态,无需多次重新认证。
Token 有效期:服务器为每个 Token 设置有效期,例如 30 分钟或 1 小时,在此期间内用户可访问受保护资源。
定期检查:前端应用会定期检查 Token 有效性,通常通过轮询或心跳机制实现,以确保用户活动期间 Token 仍有效。
刷新 Token:当服务器指示 Token 失效时,前端应用立即请求认证服务器,使用 Refresh Token 获取新的访问 Token。后端通常返回长短不同过期时间的 Token,Refresh Token 存储在 LocalStorage 中,用于获取新的访问 Token。
无缝切换:更新本地存储中的 Token 后,前端应用继续之前操作,使用户体验连贯无障碍。
设置 Token 有效期:服务器为每个 token 分配了 30 分钟的有效期,确保在一定时间内用户的身份认证保持有效。
心跳检测:前端应用通过优雅的心跳检测机制,每 5 分钟向服务器发送一个轻量级的请求,以检查当前 token 的有效性,而不是频繁地发送请求。
// 使用 setInterval 定时发送心跳请求
setInterval(async () => {
try {
await checkTokenValidity();
} catch (error) {
// 处理错误,如重试逻辑或用户通知
console.error('Error checking token validity:', error);
}
}, 5 * 60 * 1000); // 每 5 分钟检查一次
Token 有效性校验:前端应用向服务器的特定端点发送心跳请求,并附带当前 token 作为验证信息。服务器返回 token 的有效性状态。
async function checkTokenValidity() {
const token = localStorage.getItem('token');
if (!token) {
// 无 token,可能需用户重新登录
return;
}
try {
const response = await fetch('/api/heartbeat', {
headers: {
Authorization: `Bearer ${token}`
}
});
if (!response.ok) {
// Token 即将失效或已失效,进行刷新
await refreshToken();
}
} catch (error) {
// 处理网络错误或其他异常
console.error('Error checking token validity:', error);
}
}
Token 刷新机制:当 token 失效或即将失效时,前端应用使用 refresh token 向认证服务器发送请求,获取新的访问 token。
async function refreshToken() {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
// 无 refresh token,可能需用户重新登录
return;
}
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refreshToken })
});
if (response.ok) {
const data = await response.json();
// 更新本地存储的 token
localStorage.setItem('token', data.accessToken);
// 可选择性地更新有效期计时器
} else {
// 刷新 token 失败,可能需要用户重新登录或执行其他补救措施
console.error('Failed to refresh token:', response.status);
}
} catch (error) {
// 处理网络错误或其他异常
console.error('Error refreshing token:', error);
}
}
安全性考虑:
请求队列化:当开始刷新token时,将其他待发送的请求暂时放入队列中等待。一旦token刷新成功,再依次从队列中取出请求并使用新的token发送。这样可以确保在token有效之后再进行请求,避免请求失败。
使用Promise或async/await处理异步:当发送请求时,可以返回一个Promise对象,并在token刷新完成后resolve这个Promise。这样,其他请求可以await这个Promise,直到token刷新完成后再发送。
设置请求重试机制:如果请求因为token失效而失败,可以设置请求重试机制。在请求失败后,检查是否是token失效导致的,如果是,则等待token刷新完成后再次尝试发送请求。
缓存机制:对于某些可以缓存的数据,如果请求失败是因为token失效,可以先从缓存中获取数据,然后再异步刷新token和更新缓存。
降级处理:如果token刷新失败或者耗时过长,可以考虑降级处理,比如使用匿名访问或者提示用户登录。
前端状态管理:使用前端状态管理工具(如Redux, Vuex等)来管理token的刷新状态。当开始刷新token时,将状态设置为“正在刷新”,其他请求在发送前检查这个状态,如果正在刷新则等待;当token刷新成功后,更新状态并触发重新发送队列中的请求。
服务端处理:除了前端处理,服务端也可以在接收到token失效的请求时,返回特定的状态码(如401 Unauthorized),并在响应头中提供刷新token的提示或URL。前端接收到这个状态码后,可以触发token的刷新流程。
下面是一个简化的伪代码示例:
// 假设有一个请求队列
let requestQueue = [];
// 用于处理token刷新的函数
async function refreshTokenAndSendRequest(request) {
try {
// 如果队列中已经有正在进行的token刷新,则等待它完成
if (isTokenRefreshInProgress) {
return new Promise((resolve, reject) => {
requestQueue.push({ request, resolve, reject });
});
}
// 标记token刷新正在进行中
isTokenRefreshInProgress = true;
// 刷新token
const newToken = await refreshToken();
// 使用新token发送请求
const response = await request(newToken);
// 标记token刷新完成
isTokenRefreshInProgress = false;
// 处理队列中的其他请求
processQueuedRequests();
return response;
} catch (error) {
// 处理错误,如重试逻辑或用户通知
console.error('Error refreshing token and sending request:', error);
// 标记token刷新完成
isTokenRefreshInProgress = false;
// 处理队列中的其他请求(可能需要特殊处理错误情况)
processQueuedRequests();
throw error;
}
}
// 处理队列中的请求
function processQueuedRequests() {
while (requestQueue.length > 0) {
const { request, resolve, reject } = requestQueue.shift();
refreshTokenAndSendRequest(request)
.then(resolve)
.catch(reject);
}
}
// 发送请求时调用此函数
async function sendRequestWithTokenRefresh(request) {
return refreshTokenAndSendRequest(request);
}
// 示例使用
sendRequestWithTokenRefresh(someApiRequest)
.then(response => {
// 处理响应
})
.catch(error => {
// 处理错误
});
最后
还没有使用过我们刷题网站(https://fe.ecool.fun/)或者前端面试题宝典的同学,如果近期准备或者正在找工作,千万不要错过,我们的题库主打无广告和更新快哦~。
老规矩,也给我们团队的辅导服务打个广告。