1. 背景

本项目 dvp-soft-crypt 是一个微前端子应用,被集成在主应用 dvp-portal 中。其单点登录(SSO)流程依赖于主应用 dvp-portal 进行统一认证。用户在主应用登录后,会被重定向回 dvp-soft-crypt,并在 URL 中附带一个一次性的授权码(code)。子应用需要捕获这个 code,向后端换取用于后续 API 交互的 token,并最终将用户导航至其最初想要访问的页面。

2. 初始问题

在实现该流程的初期,我们遇到了两个核心问题:

  1. 二次跳转:用户在登录后返回子应用时,页面会发生两次快速的重定向,用户体验不佳。

  2. 回调路由错误:在经历两次跳转后,用户最终被定向到了微应用的基座路径 (/backstage/soft-crypt/),而不是他们最初尝试访问的深层路径(如 /backstage/soft-crypt/my-sandbox),导致用户需要重新手动导航。

3. 解决方案的演进与分析

3.1 阶段一:尝试解决二次跳转(错误的思路)

我们最初的分析集中在“竞争条件”上,即在 code 换取 token 的异步过程中,应用的其他部分(如其他 API 请求)可能因为 token 缺失而触发了 HttpService 拦截器中的登出逻辑,导致了意外的跳转。

为此,我们尝试了多种方案,例如:

  • 硬刷新页面 (window.location.reload()) :寄希望于在获取 token 后通过刷新来重置应用状态。但这破坏了单页应用(SPA)的流畅性,且由于刷新时机不可控,未能解决二次跳转问题。

  • 引入全局状态 (isLoggingIn) :试图通过一个全局标志位来“告知” HttpService 拦截器暂时不要因为 token 失效而触发登出。但由于 Vue 响应式更新的异步性以及请求发出的时机问题,这个方案也未能完美地阻止竞争条件的发生。

这些尝试都未能命中问题的核心,因为它们都试图在复杂的异步环境中“围堵”问题,而不是从流程上进行简化。

3.2 阶段二:回归本源,解决二次跳转与路由错误(最终的正确方案)

经过重新审视,我们意识到问题的根源在于脱离了单页应用(SPA)的路由控制体系。硬刷新和复杂的异步状态管理都使流程变得不可预测。

最终的、也是最简洁可靠的解决方案是:将整个登录流程完全置于 Vue Router 的控制之下,利用其导航守卫和编程式导航来管理状态。

这个流程的核心思想如下:

  1. 捕获 code 并挂起导航:在 Vue Router 的 beforeEach 全局前置守卫中,handleSSOLogin 函数检测到 URL 中的 code。它立即调用 getSoftTokenByCode API,并返回一个永远不会解析的 Promise (new Promise(() => {})) 。这一步至关重要,它会“挂起”当前的导航,防止路由在 token 获取完成前被意外地放行或取消,为异步操作争取时间。

  2. 后端设置 CookiegetSoftTokenByCode 请求成功后,我们依赖后端在响应头中通过 Set-Cookie 自动设置好 cestc-ssp-token-*。前端不再需要手动操作 Cookie。

  3. 前端路由替换 (router.replace) :当 API 调用成功后,我们不进行任何硬刷新。而是调用 Vue Router 的 router.replace() 方法,进行一次无痕的、纯前端的路由替换

  4. 保留目标路径,清洗 coderouter.replace() 的目标是用户最初想访问的路由(由 beforeEach 守卫的 to 参数提供),但我们手动从其 query 对象中删除了 code 参数。这确保了导航到正确的深层路径,同时清理了 URL。

  5. 导航守卫二次验证router.replace() 会再次触发 beforeEach 守卫。在这次执行中,handleSSOLogin 会在第一步 findSspToken() 中找到刚刚由后端设置的 Cookie。它会立即返回 true,导航被批准。

这个方案通过一次优雅的前端重定向,同时解决了二次跳转回调路由错误两个问题,流程清晰且稳定。

4. 核心代码实现

以下是最终方案的关键代码片段:

src/router/index.ts

在路由守卫中调用 handleSSOLogin 时,传入目标路由 to

typescript// ...router.beforeEach(async (to, from) => {   const commonStore = useCommonStore();  // 如果是微前端模式,则执行软密态的 SSO 登录逻辑
  if (commonStore.isMicro) {if (to.meta.loginFree) return true;// 将目标路由 to 传递给处理函数return await handleSSOLogin(to);
  }  // ...});// ...

src/utils/auth.ts

handleSSOLogin 函数的最终实现,体现了“挂起导航”和“前端重定向”的核心逻辑。

typescriptimport type { RouteLocationNormalized } from 'vue-router';import RouterFactory from '@/router';import { getSoftTokenByCode } from '@/api/auth';// ...export async function handleSSOLogin(to: RouteLocationNormalized): Promise<boolean> {  const softToken = findSspToken();  // 1. 检查 Cookie 中是否有 token
  if (softToken) {console.log('检测到 softToken,用户已登录。');return true;
  }  // 2. 检查 URL 中是否有 code
  const code = getQueryParam('code');  if (code) {console.log('在 URL 中检测到 code,正在用 code 换取 token...');const codeValue = code;getSoftTokenByCode(codeValue)
      .then(() => {console.log('code 换取 token 请求成功,执行前端重定向...');// 使用前端路由替换当前 URL,清除 code,并重新触发导航守卫const { path, query, hash } = to; // 使用传入的目标路由 toconst newQuery = { ...query };delete newQuery.code; // 清理 codeRouterFactory.router?.replace({ path, query: newQuery, hash });
      })
      .catch(error => {console.error('用 code 换取 token 时出错:', error);redirectToLogin();
      });// **关键**:在 API 请求处理期间,返回一个 pending 的 Promise 来中断当前的导航return new Promise(() => {});
  }  // 3. 如果既没有 token 也没有 code,重定向到登录页
  console.log('未检测到 token 和 code,重定向到登录页。');  redirectToLogin();  return new Promise(() => {});
}

5. 结论

通过将认证流程与 Vue Router 的导航控制机制深度结合,我们最终实现了一个健壮、平滑且符合 SPA 设计思想的单点登录流程。这个过程也证明了在处理复杂异步交互时,优先考虑利用框架自身提供的控制流工具,往往比引入额外的状态管理和副作用(如硬刷新)更为可靠和优雅。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]