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

Next.js集成MSAL React出现文本与服务端渲染HTML不匹配报错

问题根因

水合报错的核心触发逻辑:

  1. MSAL的认证状态完全依赖浏览器端存储(sessionStorage/localStorage),服务端渲染阶段不存在浏览器运行时,无法读取认证缓存,MSAL会默认返回「认证加载中」的状态,渲染加载态DOM。
  2. 客户端水合阶段可以直接读取本地存储的认证信息,若用户已登录会直接渲染受保护的内容,和服务端返回的加载态DOM结构不一致,直接触发React水合校验失败。
  3. 现有代码在_app.tsx模块顶层直接初始化PublicClientApplication实例,服务端渲染时会直接执行MSAL初始化逻辑,违反了MSAL官方「禁止在服务端调用MSAL API」的SSR适配要求。
解决方案

按以下步骤修改即可彻底解决水合不匹配问题:

  • 第一步:将MSAL实例初始化逻辑移至纯客户端侧,禁止服务端执行MSAL相关代码
    以Next.js Pages Router为例,不要在_app.tsx顶层直接初始化MSAL实例,通过动态导入禁用MSAL Provider的服务端渲染:
    // _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
    
    单独编写MSAL Provider包裹组件,所有MSAL初始化逻辑放在客户端组件内执行:
    // 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 14:15:31