如何在Next.js中使用MSAL实现双应用注册登录功能
实现Next.js中MSAL双租户/双应用注册的登录功能
我在Azure上有两个应用注册:一个面向内部用户(关联指定租户),另一个面向外部用户(关联不同租户)。我的Next.js应用登录页用MSAL实现了SSO,现在需要两个登录按钮,分别对应这两个应用注册的认证流程。但目前PublicClientApplication只能在根布局里用单一客户端ID和租户ID初始化,且需要全局共享实例,请问该怎么实现这种双登录功能?
现有代码如下:
原layout.tsx
"use client"; import { MsalProvider } from "@azure/msal-react"; import { PublicClientApplication } from "@azure/msal-browser"; import { msalConfig } from "../../msalConfig"; const msalInstance = new PublicClientApplication(msalConfig); export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <MsalProvider instance={msalInstance}> <html lang="pt-br"> <body> <div>{children}</div> </body> </html> </MsalProvider> ); }
原Page.tsx
"use client"; import { useMsal } from "@azure/msal-react"; import appRegistrationUrl from "./config/app-registration-url"; export default function LoginPage() { const { instance } = useMsal(); const handleLoginMsalExternalUser = () => { instance .loginRedirect({ prompt: "create", scopes: ["User.Read", appRegistrationUrl], }) .catch((e) => { console.error(e); }); }; const handleLoginMsalInternalUser = () => { instance .loginRedirect({ prompt: "create", scopes: ["User.Read", appRegistrationUrl], }) .catch((e) => { console.error(e); }); }; return ( <div> <button type="button" onClick={handleLoginMsalInternalUser}> <span>Internal User login</span> </button> <button type="button" onClick={handleLoginMsalExternalUser}> <span>External User login</span> </button> </div> ); }
解决方案
1. 配置两个MSAL实例的参数
首先在msalConfig.ts中定义两组独立配置,分别对应内部和外部应用注册:
// msalConfig.ts export const msalInternalConfig = { auth: { clientId: "YOUR_INTERNAL_CLIENT_ID", // 内部应用注册的客户端ID authority: "https://login.microsoftonline.com/YOUR_INTERNAL_TENANT_ID", // 内部租户ID redirectUri: "/", // 根据应用实际配置调整 postLogoutRedirectUri: "/", }, cache: { cacheLocation: "sessionStorage", storeAuthStateInCookie: false, cacheKey: "msal_internal_cache", // 自定义缓存键,避免与外部实例缓存冲突 }, }; export const msalExternalConfig = { auth: { clientId: "YOUR_EXTERNAL_CLIENT_ID", // 外部应用注册的客户端ID authority: "https://login.microsoftonline.com/YOUR_EXTERNAL_TENANT_ID", // 外部租户ID,多租户场景可用`common`/`organizations` redirectUri: "/", postLogoutRedirectUri: "/", }, cache: { cacheLocation: "sessionStorage", storeAuthStateInCookie: false, cacheKey: "msal_external_cache", // 独立缓存键 }, };
2. 创建自定义Context管理双MSAL实例
由于MsalProvider仅支持绑定单个实例,我们用React Context全局共享两个实例:
// app/providers/MsalMultiInstanceProvider.tsx "use client"; import { createContext, useContext, ReactNode } from "react"; import { PublicClientApplication } from "@azure/msal-browser"; import { msalInternalConfig, msalExternalConfig } from "../../msalConfig"; // 初始化两个独立的MSAL实例 const internalMsalInstance = new PublicClientApplication(msalInternalConfig); const externalMsalInstance = new PublicClientApplication(msalExternalConfig); // 创建Context const MsalMultiInstanceContext = createContext({ internalInstance: internalMsalInstance, externalInstance: externalMsalInstance, }); export function MsalMultiInstanceProvider({ children }: { children: ReactNode }) { return ( <MsalMultiInstanceContext.Provider value={{ internalInstance, externalInstance }}> {children} </MsalMultiInstanceContext.Provider> ); } // 自定义Hook,方便组件获取实例 export function useMsalMultiInstance() { return useContext(MsalMultiInstanceContext); }
3. 修改根布局使用自定义Provider
替换原有的MsalProvider为我们的多实例Provider:
// app/layout.tsx "use client"; import { MsalMultiInstanceProvider } from "./providers/MsalMultiInstanceProvider"; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <MsalMultiInstanceProvider> <html lang="pt-br"> <body> <div>{children}</div> </body> </html> </MsalMultiInstanceProvider> ); }
4. 修改登录页调用对应实例
在登录页中获取两个实例,分别处理内部和外部用户的认证流程:
// app/login/page.tsx "use client"; import { useMsalMultiInstance } from "../providers/MsalMultiInstanceProvider"; export default function LoginPage() { const { internalInstance, externalInstance } = useMsalMultiInstance(); // 内部用户登录逻辑 const handleLoginInternal = () => { internalInstance .loginRedirect({ prompt: "select_account", // 推荐用该值,避免强制创建新账号 scopes: ["User.Read", "YOUR_INTERNAL_API_SCOPE"], // 替换为内部应用的权限范围 }) .catch((e) => console.error("内部登录失败:", e)); }; // 外部用户登录逻辑 const handleLoginExternal = () => { externalInstance .loginRedirect({ prompt: "select_account", scopes: ["User.Read", "YOUR_EXTERNAL_API_SCOPE"], // 替换为外部应用的权限范围 }) .catch((e) => console.error("外部登录失败:", e)); }; return ( <div style={{ padding: "2rem" }}> <button type="button" onClick={handleLoginInternal} style={{ marginRight: "1rem", padding: "0.5rem 1rem" }} > 内部用户登录 </button> <button type="button" onClick={handleLoginExternal} style={{ padding: "0.5rem 1rem" }} > 外部用户登录 </button> </div> ); }
关键注意事项
- 缓存隔离:通过设置不同的
cacheKey确保两个实例的认证缓存互不干扰,避免登录状态混乱。 - 权限范围:每个登录请求的
scopes必须对应各自应用注册配置的权限,禁止混用。 - 租户适配:外部应用若支持所有Azure AD租户,可将
authority设为https://login.microsoftonline.com/common或organizations。 - 实例初始化:两个
PublicClientApplication实例必须在客户端组件(带"use client"指令)中初始化,确保在浏览器环境运行。
内容的提问来源于stack exchange,提问作者Agner Souza Bezerra
相关产品推荐
相关产品推荐

