You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用MSAL Browser与Vue Router导航守卫安全保护Vue3 SPA

使用MSAL-browser重定向流+Vue Router守卫保护SPA的问题

预期目标

保护路由/items,仅允许已认证用户访问。用户直接输入https://localhost/items时,未认证则跳转至微软认证页面,认证成功后返回原页面。

现有问题

  1. 未认证时输入/items,页面会短暂闪现后才跳转至认证页,不符合预期。
  2. 导航守卫的嵌套Promise代码结构繁琐,改用async/await重构后,认证成功后跳转到/redirect空白页,而非预期的/items。

环境信息

  • Vue.js: 3.3.4
  • Vue Router: 4.1.6
  • msal-browser: 3.1.0
  • TypeScript

我的代码

认证配置

import { LogLevel, PublicClientApplication } from "@azure/msal-browser";

const isIE =
  window.navigator.userAgent.indexOf("MSIE ") > -1 ||
  window.navigator.userAgent.indexOf("Trident/") > -1;

export const msalConfig = {
  auth: {
    clientId: "myClientId",
    authority: "myAuthority",
    redirectUri: "/redirect",
    postLogoutRedirectUri: "https://account.microsoft.com/account",
    navigateToLoginRequestUrl: true,
  },
  cache: {
    cacheLocation: "localStorage",
    storeAuthStateInCookie: isIE,
  }
};

export const msalInstance = new PublicClientApplication(msalConfig);

Vue Router配置

const routes: Array<RouteRecordRaw> = [
  {
    path: "/items",
    name: "items",
    component: TheItems,
    meta: {
      requiresRoles: routeRoleAccess.items,
    },
  },
  {
    path: "/redirect",
    name: "redirect",
    component: TheAuthRedirect,
  },
  {
    path: "/:pathMatch(.*)*",
    name: "notFound",
    component: TheNotFoundPage,
  },
];

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: routes,
});

// 原嵌套Promise的守卫代码
router.beforeResolve(
  async (to: RouteLocationNormalized, from: RouteLocationNormalized) => {
    const unauthorizedRoute = { name: "notFound" };

    if (!to.meta?.requiresRoles) {
      return true;
    }

    return msalInstance
      .acquireTokenSilent(loginRequest)
      .then(async (authResult) => {
        saveAuthData(authResult);
        return hasAccessTo(to.name as string) || unauthorizedRoute;
      })
      .catch(() => {
        msalInstance
          .handleRedirectPromise()
          .then(() => {
            return msalInstance
              .loginRedirect()
              .then(async () => {
                const authResult = await msalInstance.acquireTokenSilent(loginRequest);
                saveAuthData(authResult);
                return hasAccessTo(to.name as string) || unauthorizedRoute;
              })
              .catch(() => {
                return unauthorizedRoute;
              });
          })
          .catch(() => {
            return unauthorizedRoute;
          });
      });
  }
);

export default router;

已尝试方案

  1. 切换使用beforeEach()、beforeResolve()、beforeEnter()守卫
  2. 重构为async/await代码,但登录后跳转至/redirect而非/items:
try {
  const authResult = await msalInstance.acquireTokenSilent(loginRequest);
  saveAuthData(authResult);
  return hasAccessTo(to.name as string) || unauthorizedRoute;
} catch {
  try {
    await msalInstance.handleRedirectPromise();
    await msalInstance.loginRedirect();
    const authResult = await msalInstance.acquireTokenSilent(loginRequest);
    saveAuthData(authResult);
    return hasAccessTo(to.name as string) || unauthorizedRoute;
  } catch {
    return unauthorizedRoute;
  }
}

解决方案

1. 解决页面短暂闪现问题

将导航守卫从beforeResolve改为beforeEach,beforeResolve会在组件即将解析时触发,此时组件已开始渲染,导致闪现;beforeEach在导航触发时立即执行,能更早拦截未授权访问。

同时在应用初始化时提前处理MSAL重定向Promise,确保认证状态在路由守卫执行前确定:

// main.ts
import { msalInstance } from './authConfig';
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

async function initApp() {
  // 先处理重定向回调,确保账号信息已加载
  await msalInstance.handleRedirectPromise();
  createApp(App).use(router).mount('#app');
}

initApp();

2. 修复async/await重构后的跳转问题

loginRedirect是页面跳转操作,执行后当前页面会被卸载,后续代码不会运行。因此守卫中调用loginRedirect后无需再执行后续逻辑,MSAL会通过navigateToLoginRequestUrl: true自动跳回原请求URL。

重构后的路由守卫代码:

router.beforeEach(async (to, from) => {
  const unauthorizedRoute = { name: "notFound" };

  if (!to.meta?.requiresRoles) {
    return true;
  }

  // 检查是否存在已登录账号
  const accounts = msalInstance.getAllAccounts();
  if (accounts.length === 0) {
    // 未登录,发起重定向登录
    await msalInstance.loginRedirect({
      scopes: loginRequest.scopes
    });
    // 返回false阻止当前导航,因为页面即将跳转
    return false;
  }

  try {
    // 已登录,静默获取令牌
    const authResult = await msalInstance.acquireTokenSilent(loginRequest);
    saveAuthData(authResult);
    // 校验角色权限
    return hasAccessTo(to.name as string) || unauthorizedRoute;
  } catch (error) {
    // 静默获取失败,发起交互式令牌获取
    await msalInstance.acquireTokenRedirect(loginRequest);
    return false;
  }
});

3. 优化/redirect路由

/redirect路由无需实际业务组件,可简化为:

{
  path: "/redirect",
  name: "redirect",
  component: () => null, // 或添加简单加载提示
},

核心逻辑说明

  • navigateToLoginRequestUrl: true会让MSAL自动记录用户最初请求的URL,登录成功后自动跳转回去,无需手动处理。
  • 应用启动时必须先调用handleRedirectPromise(),确保重定向后的认证结果被正确处理,账号信息存入缓存。
  • 调用loginRedirect/acquireTokenRedirect后必须返回false,阻止当前导航流程,因为页面即将跳转,继续执行无意义。

内容的提问来源于stack exchange,提问作者uodami

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 03:54:56