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

Next.js 14 App Router集成Azure MSAL SSO配置问题求助

Next.js 14 App Router 下 Azure MSAL 单点登录配置问题及解决方案

问题描述

基于Next.js v14.1.0 App Router开发React应用,以app目录的layout.tsx为入口,尝试通过Azure MSAL实现单点登录(SSO)。当前应用可跳转至微软登录页,但输入有效Azure AD凭据后,会重定向回UnauthenticatedTemplate,且accounts数组为空。相同凭据可正常登录其他应用,推测App Router场景需要特殊配置。

当前配置

1. layout.tsx

"use client";
import React, { useEffect, useRef } from "react";
import "./globals.css";
import { Box } from "@mui/material";
import { AppStore, makeStore, persistor } from "@/lib/store";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { msalConfig } from "@/lib/Msal";
import { EventType, PublicClientApplication } from "@azure/msal-browser";
import {
  AuthenticatedTemplate,
  MsalProvider,
  UnauthenticatedTemplate,
  useIsAuthenticated,
} from "@azure/msal-react";
import Home from "./page";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  const msalInstance = new PublicClientApplication(msalConfig);
  const storeRef = useRef<AppStore>();
  if (!storeRef.current) {
    storeRef.current = makeStore();
  }
  const isAuthenticated = useIsAuthenticated();
  console.log("isAuthenticated", isAuthenticated);

  msalInstance.initialize().then(() => {
    const accounts = msalInstance.getAllAccounts();
    if (accounts.length > 0) {
      msalInstance.setActiveAccount(accounts[0]);
    }

    msalInstance.addEventCallback((event) => {
      if (
        event.eventType === EventType.LOGIN_SUCCESS &&
        event.payload &&
        "account" in event.payload &&
        event.payload.account
      ) {
        const account = event.payload.account;
        msalInstance.setActiveAccount(account);
      }
    });
  });
  return (
    <html lang="en">
      <body>
        <Provider store={storeRef.current}>
          <PersistGate loading={null} persistor={storeRef.current.__persistor}>
            <MsalProvider instance={msalInstance}>
              <Box sx={{ display: "flex" }}>
                <UnauthenticatedTemplate>
                  <Home />
                </UnauthenticatedTemplate>
                <AuthenticatedTemplate>{children}</AuthenticatedTemplate>
              </Box>
            </MsalProvider>
          </PersistGate>
        </Provider>
      </body>
    </html>
  );
}

2. Msal.ts

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

export const msalConfig: Configuration = {
  auth: {
    clientId: process.env.NEXT_PUBLIC_AZURE_AD_CLIENT_ID || "default-client-id", // Azure AD client id
    authority: `https://login.microsoftonline.com/${process.env.NEXT_PUBLIC_AZURE_AD_TENANT_ID}`, // Azure AD tenant id
    redirectUri: process.env.NEXT_PUBLIC_APP_REDIRECT_URL, // app redirect URL
    postLogoutRedirectUri: process.env.REACT_APP_BASE_URL + "/", // app logout URL
    navigateToLoginRequestUrl: false,
  },
  cache: {
    cacheLocation: "sessionStorage", // This configures where your cache will be stored
    storeAuthStateInCookie: false, // If you are having issues on IE11, you may need to set this to true
  },
  system: {
    loggerOptions: {
      loggerCallback: (level, message, containsPii) => {
        if (containsPii) {
          return;
        }
        switch (level) {
          case LogLevel.Error:
            console.error(message);
            return;
          case LogLevel.Info:
            console.info(message);
            return;
          case LogLevel.Verbose:
            console.debug(message);
            return;
          case LogLevel.Warning:
            console.warn(message);
            return;
        }
      },
      logLevel: LogLevel.Verbose,
      piiLoggingEnabled: false,
    },
  },
};

export const loginRequest = {
  scopes: ["User.Read"],
};

export const graphConfig = {
  graphMeEndpoint: "https://graph.microsoft.com/v1.0/me",
};

3. 登录方法

import { useMsal } from "@azure/msal-react";
import { loginRequest } from "@/lib/Msal";
const { instance } = useMsal();
const handleLoginClick = async () => {
  instance
    .loginRedirect(loginRequest)
    .catch((err) => console.error("login failed", err));
};

日志信息

: @azure/msal-browser@3.10.0 : Info - Emitting event: msal:initializeEnd
Msal.ts:36 : @azure/msal-browser@3.10.0 : Info - Emitting event: msal:handleRedirectStart
Msal.ts:36 : @azure/msal-browser@3.10.0 : Info - Emitting event: msal:loginFailure
Msal.ts:36 : @azure/msal-browser@3.10.0 : Info - Emitting event: msal:handleRedirectEnd
Msal.ts:36 : @azure/msal-react@2.0.12 : Info - MsalProvider - msal:handleRedirectStart results in setting inProgress from startup to handleRedirect
Msal.ts:36 : @azure/msal-react@2.0.12 : Info - MsalProvider - msal:handleRedirectStart results in setting inProgress from startup to handleRedirect
Msal.ts:36 : @azure/msal-react@2.0.12 : Info - MsalProvider - msal:handleRedirectEnd results in setting inProgress from handleRedirect to none
Msal.ts:36 : @azure/msal-react@2.0.12 : Info - MsalProvider - msal:handleRedirectEnd results in setting inProgress from handleRedirect to none

解决方案

核心问题分析

日志显示msal:loginFailure,结合App Router特性,核心问题包括:

  • MSAL实例重复创建:每次layout.tsx渲染都会生成新的PublicClientApplication实例,导致缓存丢失
  • 组件层级错误:MsalProvider被嵌套在Redux Provider内部,部分子组件无法正常访问MSAL上下文
  • 环境变量不一致:postLogoutRedirectUri使用非NEXT_PUBLIC_前缀的变量,无法被客户端读取
  • 重定向回调未正确处理:App Router的路由行为需要显式处理MSAL的重定向Promise

修正步骤

1. 创建全局MSAL实例

新建@/lib/msalInstance.ts,确保实例全局唯一,避免重复初始化:

import { PublicClientApplication } from "@azure/msal-browser";
import { msalConfig } from "./Msal";

export const msalInstance = new PublicClientApplication(msalConfig);

// 初始化并加载缓存账户
msalInstance.initialize().then(() => {
  const accounts = msalInstance.getAllAccounts();
  if (accounts.length > 0) {
    msalInstance.setActiveAccount(accounts[0]);
  }

  // 监听登录成功事件,设置活跃账户
  msalInstance.addEventCallback((event) => {
    if (event.eventType === "msal:loginSuccess" && event.payload?.account) {
      msalInstance.setActiveAccount(event.payload.account);
    }
  });
});

2. 修正layout.tsx组件层级

调整MsalProvider到最外层,确保所有子组件能访问MSAL上下文:

"use client";
import React, { useRef } from "react";
import "./globals.css";
import { Box } from "@mui/material";
import { AppStore, makeStore, persistor } from "@/lib/store";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { msalInstance } from "@/lib/msalInstance";
import {
  AuthenticatedTemplate,
  MsalProvider,
  UnauthenticatedTemplate,
} from "@azure/msal-react";
import Home from "./page";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  const storeRef = useRef<AppStore>();
  if (!storeRef.current) {
    storeRef.current = makeStore();
  }

  return (
    <html lang="en">
      <body>
        <MsalProvider instance={msalInstance}>
          <Provider store={storeRef.current}>
            <PersistGate loading={null} persistor={storeRef.current.__persistor}>
              <Box sx={{ display: "flex" }}>
                <UnauthenticatedTemplate>
                  <Home />
                </UnauthenticatedTemplate>
                <AuthenticatedTemplate>{children}</AuthenticatedTemplate>
              </Box>
            </PersistGate>
          </Provider>
        </MsalProvider>
      </body>
    </html>
  );
}

3. 统一环境变量前缀

将Msal.ts中的postLogoutRedirectUri改为NEXT_PUBLIC_前缀:

auth: {
  // ...其他配置
  postLogoutRedirectUri: `${process.env.NEXT_PUBLIC_APP_REDIRECT_URL}/`,
}

4. 在登录页处理重定向回调

修改app/page.tsx,显式处理MSAL的重定向Promise:

"use client";
import { useMsal } from "@azure/msal-react";
import { useEffect } from "react";

export default function Home() {
  const { instance, inProgress } = useMsal();

  useEffect(() => {
    if (inProgress === "none") {
      instance.handleRedirectPromise().catch(err => {
        console.error("重定向处理失败:", err);
      });
    }
  }, [instance, inProgress]);

  const handleLoginClick = async () => {
    await instance.loginRedirect({ scopes: ["User.Read"] });
  };

  return (
    <div>
      <button onClick={handleLoginClick}>登录</button>
    </div>
  );
}

额外检查项

  • 确认Azure AD应用注册中的重定向URI与NEXT_PUBLIC_APP_REDIRECT_URL完全一致(包含协议、域名、端口)
  • 若浏览器阻止sessionStorage,可临时切换为localStorage测试缓存功能
  • 确保@azure/msal-browser@3.x与@azure/msal-react@2.x版本兼容,无需降级

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 20:00:13