Next.js集成MSAL React出现文本与服务端渲染HTML不匹配报错
问题根因
水合报错的核心触发逻辑:
- MSAL的认证状态完全依赖浏览器端存储(
sessionStorage/localStorage),服务端渲染阶段不存在浏览器运行时,无法读取认证缓存,MSAL会默认返回「认证加载中」的状态,渲染加载态DOM。 - 客户端水合阶段可以直接读取本地存储的认证信息,若用户已登录会直接渲染受保护的内容,和服务端返回的加载态DOM结构不一致,直接触发React水合校验失败。
- 现有代码在
_app.tsx模块顶层直接初始化PublicClientApplication实例,服务端渲染时会直接执行MSAL初始化逻辑,违反了MSAL官方「禁止在服务端调用MSAL API」的SSR适配要求。
解决方案
按以下步骤修改即可彻底解决水合不匹配问题:
- 第一步:将MSAL实例初始化逻辑移至纯客户端侧,禁止服务端执行MSAL相关代码
以Next.js Pages Router为例,不要在_app.tsx顶层直接初始化MSAL实例,通过动态导入禁用MSAL Provider的服务端渲染:
单独编写MSAL Provider包裹组件,所有MSAL初始化逻辑放在客户端组件内执行:// _app.tsx import type { AppProps } from 'next/app' import dynamic from 'next/dynamic' // 动态导入MSAL Provider包裹组件,关闭该组件的SSR const MsalProviderWrapper = dynamic(() => import('../components/MsalProviderWrapper'), { ssr: false, }) function MyApp({ Component, pageProps }: AppProps) { return ( <MsalProviderWrapper> <Component {...pageProps} /> </MsalProviderWrapper> ) } export default MyApp// components/MsalProviderWrapper.tsx import { PublicClientApplication, LogLevel } from "@azure/msal-browser"; import { MsalProvider } from "@azure/msal-react"; import { ReactNode, useMemo } from "react"; const MsalProviderWrapper = ({ children }: { children: ReactNode }) => { // 用useMemo保证MSAL实例仅在客户端初始化一次 const msalInstance = useMemo(() => { return new PublicClientApplication({ auth: { clientId: process.env.NEXT_PUBLIC_B2C_WEBAPP_APP_ID as string, redirectUri: process.env.NEXT_PUBLIC_B2C_REDIRECT_URI, authority: `https://${process.env.NEXT_PUBLIC_B2C_TENANT_NAME}.b2clogin.com/${process.env.NEXT_PUBLIC_B2C_TENANT_NAME}.onmicrosoft.com/${process.env.NEXT_PUBLIC_B2C_SIGNIN_SIGNUP_POLICY}`, knownAuthorities: [ `${process.env.NEXT_PUBLIC_B2C_TENANT_NAME}.b2clogin.com`, ], navigateToLoginRequestUrl: false, }, cache: { cacheLocation: "sessionStorage", storeAuthStateInCookie: false, }, system: { loggerOptions: { logLevel: LogLevel.Verbose, 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; } }, }, }, }); }, []); return <MsalProvider instance={msalInstance}>{children}</MsalProvider> } export default MsalProviderWrapper; - 第二步:业务页面增加客户端挂载判断,保证水合阶段两端DOM完全一致
不要在组件首次渲染时直接读取MSAL认证状态或渲染认证组件,等组件确认在客户端挂载完成后再执行认证逻辑,未挂载时统一返回和服务端一致的占位内容。
使用MsalAuthenticationTemplate的页面正确写法:
使用// pages/create-advert.tsx import { InteractionType } from "@azure/msal-browser"; import { MsalAuthenticationTemplate } from "@azure/msal-react"; import { NextPage } from "next"; import { useState, useEffect } from "react"; const ErrorComponent = ({ error }: any) => { return <p>an Error occured: {error}</p>; }; const LoadingComponent = () => { return <p>Authentication in progress...</p>; }; const CreateAdvert: NextPage = () => { const [isMounted, setIsMounted] = useState(false); useEffect(() => { setIsMounted(true); }, []); // 未完成客户端挂载时,统一返回占位内容,和服务端渲染结果对齐 if (!isMounted) return <p>Authentication in progress...</p>; return ( <> <MsalAuthenticationTemplate interactionType={InteractionType.Redirect} errorComponent={ErrorComponent} loadingComponent={LoadingComponent} > <p>content</p> </MsalAuthenticationTemplate> </> ); }; export default CreateAdvert;useIsAuthenticated钩子的简化写法:// pages/create-advert.tsx import { useIsAuthenticated } from "@azure/msal-react"; import { NextPage } from "next"; import { useState, useEffect } from "react"; const CreateAdvert: NextPage = () => { const isAuthenticated = useIsAuthenticated(); const [isMounted, setIsMounted] = useState(false); useEffect(() => { setIsMounted(true); }, []); if (!isMounted) return <p>Loading...</p>; if (!isAuthenticated) return <p>not logged in!</p>; return <p>logged in!</p>; }; export default CreateAdvert; - 适配说明:如果使用Next.js App Router,直接在
MsalProviderWrapper组件顶部加'use client'指令,所有调用MSAL钩子的组件也标记为客户端组件,配合上述挂载判断逻辑即可正常运行。
注意事项
- 不要在服务端组件、模块顶层直接实例化
PublicClientApplication,所有MSAL相关操作必须在客户端侧执行。 - 水合校验要求服务端和客户端首次渲染的DOM结构完全一致,不要在首次渲染时直接依赖仅客户端存在的存储、API返回值。
- 如果遇到重定向回调后认证状态读取异常,可以临时将
storeAuthStateInCookie配置改为true,适配部分浏览器对跳转场景下sessionStorage的读取限制,问题解决后可改回false提升安全性。
内容的提问来源于stack exchange,提问作者J. Reku
相关产品推荐
相关产品推荐

