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
相关产品推荐
相关产品推荐

