面试官:如何处理单点登录(SSO)的无感刷新问题?

大家好,今天的分享由团队的 uncle13 老师提供。

在大型企业或复杂的应用生态系统中,单点登录是一种常见的认证方式。用户只需登录一次,就可以访问多个应用或服务。

在这些场景中,无感刷新 Token 可以确保用户在整个会话期间保持登录状态,无需多次重新认证。

实现原理

  1. Token 有效期:服务器为每个 Token 设置有效期,例如 30 分钟或 1 小时,在此期间内用户可访问受保护资源。

  2. 定期检查:前端应用会定期检查 Token 有效性,通常通过轮询或心跳机制实现,以确保用户活动期间 Token 仍有效。

  3. 刷新 Token:当服务器指示 Token 失效时,前端应用立即请求认证服务器,使用 Refresh Token 获取新的访问 Token。后端通常返回长短不同过期时间的 Token,Refresh Token 存储在 LocalStorage 中,用于获取新的访问 Token。

  4. 无缝切换:更新本地存储中的 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'
      },
      bodyJSON.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 也应设定有效期,并在过期后要求用户重新登录,以增强安全性。
  • 在实际应用中,还应考虑使用 HTTPS 来保护传输过程中的 token,防止中间人攻击。
  • 对于敏感操作,如更改密码或进行高价值交易,可能还需要额外的验证步骤,如短信验证或生物识别。

token还没有刷新完成时,发送了其他请求需要如何处理?

  1. 请求队列化:当开始刷新token时,将其他待发送的请求暂时放入队列中等待。一旦token刷新成功,再依次从队列中取出请求并使用新的token发送。这样可以确保在token有效之后再进行请求,避免请求失败。

  2. 使用Promise或async/await处理异步:当发送请求时,可以返回一个Promise对象,并在token刷新完成后resolve这个Promise。这样,其他请求可以await这个Promise,直到token刷新完成后再发送。

  3. 设置请求重试机制:如果请求因为token失效而失败,可以设置请求重试机制。在请求失败后,检查是否是token失效导致的,如果是,则等待token刷新完成后再次尝试发送请求。

  4. 缓存机制:对于某些可以缓存的数据,如果请求失败是因为token失效,可以先从缓存中获取数据,然后再异步刷新token和更新缓存。

  5. 降级处理:如果token刷新失败或者耗时过长,可以考虑降级处理,比如使用匿名访问或者提示用户登录。

  6. 前端状态管理:使用前端状态管理工具(如Redux, Vuex等)来管理token的刷新状态。当开始刷新token时,将状态设置为“正在刷新”,其他请求在发送前检查这个状态,如果正在刷新则等待;当token刷新成功后,更新状态并触发重新发送队列中的请求。

  7. 服务端处理:除了前端处理,服务端也可以在接收到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/)或者前端面试题宝典的同学,如果近期准备或者正在找工作,千万不要错过,我们的题库主打无广告和更新快哦~。

老规矩,也给我们团队的辅导服务打个广告。