新江北客户端
152.16MB · 2025-12-13
本项目 dvp-soft-crypt 是一个微前端子应用,被集成在主应用 dvp-portal 中。其单点登录(SSO)流程依赖于主应用 dvp-portal 进行统一认证。用户在主应用登录后,会被重定向回 dvp-soft-crypt,并在 URL 中附带一个一次性的授权码(code)。子应用需要捕获这个 code,向后端换取用于后续 API 交互的 token,并最终将用户导航至其最初想要访问的页面。
在实现该流程的初期,我们遇到了两个核心问题:
二次跳转:用户在登录后返回子应用时,页面会发生两次快速的重定向,用户体验不佳。
回调路由错误:在经历两次跳转后,用户最终被定向到了微应用的基座路径 (/backstage/soft-crypt/),而不是他们最初尝试访问的深层路径(如 /backstage/soft-crypt/my-sandbox),导致用户需要重新手动导航。
我们最初的分析集中在“竞争条件”上,即在 code 换取 token 的异步过程中,应用的其他部分(如其他 API 请求)可能因为 token 缺失而触发了 HttpService 拦截器中的登出逻辑,导致了意外的跳转。
为此,我们尝试了多种方案,例如:
硬刷新页面 (window.location.reload()) :寄希望于在获取 token 后通过刷新来重置应用状态。但这破坏了单页应用(SPA)的流畅性,且由于刷新时机不可控,未能解决二次跳转问题。
引入全局状态 (isLoggingIn) :试图通过一个全局标志位来“告知” HttpService 拦截器暂时不要因为 token 失效而触发登出。但由于 Vue 响应式更新的异步性以及请求发出的时机问题,这个方案也未能完美地阻止竞争条件的发生。
这些尝试都未能命中问题的核心,因为它们都试图在复杂的异步环境中“围堵”问题,而不是从流程上进行简化。
经过重新审视,我们意识到问题的根源在于脱离了单页应用(SPA)的路由控制体系。硬刷新和复杂的异步状态管理都使流程变得不可预测。
最终的、也是最简洁可靠的解决方案是:将整个登录流程完全置于 Vue Router 的控制之下,利用其导航守卫和编程式导航来管理状态。
这个流程的核心思想如下:
捕获 code 并挂起导航:在 Vue Router 的 beforeEach 全局前置守卫中,handleSSOLogin 函数检测到 URL 中的 code。它立即调用 getSoftTokenByCode API,并返回一个永远不会解析的 Promise (new Promise(() => {})) 。这一步至关重要,它会“挂起”当前的导航,防止路由在 token 获取完成前被意外地放行或取消,为异步操作争取时间。
后端设置 Cookie:getSoftTokenByCode 请求成功后,我们依赖后端在响应头中通过 Set-Cookie 自动设置好 cestc-ssp-token-*。前端不再需要手动操作 Cookie。
前端路由替换 (router.replace) :当 API 调用成功后,我们不进行任何硬刷新。而是调用 Vue Router 的 router.replace() 方法,进行一次无痕的、纯前端的路由替换。
保留目标路径,清洗 code:router.replace() 的目标是用户最初想访问的路由(由 beforeEach 守卫的 to 参数提供),但我们手动从其 query 对象中删除了 code 参数。这确保了导航到正确的深层路径,同时清理了 URL。
导航守卫二次验证:router.replace() 会再次触发 beforeEach 守卫。在这次执行中,handleSSOLogin 会在第一步 findSspToken() 中找到刚刚由后端设置的 Cookie。它会立即返回 true,导航被批准。
这个方案通过一次优雅的前端重定向,同时解决了二次跳转和回调路由错误两个问题,流程清晰且稳定。
以下是最终方案的关键代码片段:
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.tshandleSSOLogin 函数的最终实现,体现了“挂起导航”和“前端重定向”的核心逻辑。
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(() => {});
}通过将认证流程与 Vue Router 的导航控制机制深度结合,我们最终实现了一个健壮、平滑且符合 SPA 设计思想的单点登录流程。这个过程也证明了在处理复杂异步交互时,优先考虑利用框架自身提供的控制流工具,往往比引入额外的状态管理和副作用(如硬刷新)更为可靠和优雅。