SvelteKit结合MSAL.js与Azure AD B2C时遇non_browser_environment错误
解决SvelteKit + MSAL.js + Azure AD B2C的服务端认证错误问题
你的核心问题在于混淆了MSAL的浏览器版与Node.js版的适用场景:hooks.server.ts运行在服务端环境,没有浏览器的window对象,直接调用@azure/msal-browser的方法(包括handleRedirectPromise)必然会抛出non_browser_environment错误。下面是具体的解决方案:
1. 拆分MSAL实例:客户端与服务端分离
MSAL分为两个包:
@azure/msal-browser:仅用于浏览器客户端,处理前端登录、静默获取token等操作@azure/msal-node:用于服务端,处理授权码流程的服务端逻辑(生成登录URL、验证回调、获取token等)
服务端MSAL配置(src/lib/auth.server.ts)
import { ConfidentialClientApplication } from '@azure/msal-node'; import { env } from '$env/dynamic/private'; const msalConfig = { auth: { clientId: env.AZURE_AD_B2C_CLIENT_ID, authority: `https://${env.AZURE_AD_B2C_TENANT_NAME}.b2clogin.com/${env.AZURE_AD_B2C_TENANT_NAME}.onmicrosoft.com/${env.AZURE_AD_B2C_POLICY_NAME}`, clientSecret: env.AZURE_AD_B2C_CLIENT_SECRET } }; export const msalServerClient = new ConfidentialClientApplication(msalConfig);
客户端MSAL配置(src/lib/auth.ts)
import { PublicClientApplication } from '@azure/msal-browser'; import { env } from '$env/dynamic/public'; const msalConfig = { auth: { clientId: env.PUBLIC_AZURE_AD_B2C_CLIENT_ID, authority: `https://${env.PUBLIC_AZURE_AD_B2C_TENANT_NAME}.b2clogin.com/${env.PUBLIC_AZURE_AD_B2C_TENANT_NAME}.onmicrosoft.com/${env.PUBLIC_AZURE_AD_B2C_POLICY_NAME}`, redirectUri: `${env.PUBLIC_APP_BASE_URL}/auth/callback` } }; export const msalClient = new PublicClientApplication(msalConfig); // 客户端处理重定向回调(仅在浏览器环境执行) msalClient.handleRedirectPromise().then(account => { if (account) { // 前端登录成功后的逻辑,比如更新用户状态 } }).catch(err => console.error('前端重定向回调失败:', err)); export const signIn = () => { msalClient.loginRedirect({ scopes: ['openid', 'profile', env.PUBLIC_AZURE_AD_B2C_API_SCOPE] }); };
2. 重写hooks.server.ts的认证逻辑
不要在服务端调用浏览器版MSAL的方法,改用msal-node生成登录URL并处理重定向:
import type { Handle } from '@sveltejs/kit'; import { msalServerClient } from '$lib/auth.server'; import { env } from '$env/dynamic/private'; export const handle: Handle = async ({ event, resolve }) => { // 从cookie/session中读取已登录用户信息(需提前在回调路由存入) const userCookie = event.cookies.get('auth_user'); const loggedInUser = userCookie ? JSON.parse(userCookie) : null; // 定义需要认证保护的路由 const protectedRoutes = ['/dashboard', '/profile']; const isProtected = protectedRoutes.some(route => event.url.pathname.startsWith(route)); if (isProtected && !loggedInUser) { // 生成Azure AD B2C的登录URL const authUrlParams = { scopes: ['openid', 'profile', env.AZURE_AD_B2C_API_SCOPE], redirectUri: `${env.APP_BASE_URL}/auth/callback` }; const authUrl = await msalServerClient.getAuthCodeUrl(authUrlParams); // 重定向到登录页 return new Response(null, { status: 302, headers: { Location: authUrl } }); } // 将用户信息挂载到event.locals,供页面组件使用 event.locals.user = loggedInUser; return resolve(event); };
3. 实现登录回调路由(src/routes/auth/callback/+server.ts)
服务端处理Azure AD B2C的回调,获取token并存入cookie:
import { msalServerClient } from '$lib/auth.server'; import { env } from '$env/dynamic/private'; import type { RequestHandler } from './$types'; export const GET: RequestHandler = async ({ url, cookies }) => { const code = url.searchParams.get('code'); if (!code) return new Response('授权码缺失', { status: 400 }); try { const tokenRequest = { code, scopes: ['openid', 'profile', env.AZURE_AD_B2C_API_SCOPE], redirectUri: `${env.APP_BASE_URL}/auth/callback` }; const authResponse = await msalServerClient.acquireTokenByCode(tokenRequest); // 将用户信息存入HttpOnly cookie(更安全) cookies.set('auth_user', JSON.stringify(authResponse.account), { path: '/', httpOnly: true, secure: env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 3600 * 24 // 1天有效期 }); // 重定向到首页或之前的路由 return new Response(null, { status: 302, headers: { Location: '/' } }); } catch (err) { return new Response('认证失败', { status: 500 }); } };
4. 实现API请求拦截(类似HttpInterceptor)
客户端拦截(src/hooks.client.ts)
在前端请求时自动带上access token:
import type { HandleClientError } from '@sveltejs/kit'; import { msalClient } from '$lib/auth'; export const handleFetch = async ({ request, fetch }) => { // 仅对需要认证的API添加token if (request.url.startsWith(env.PUBLIC_API_BASE_URL)) { const account = msalClient.getActiveAccount(); if (account) { const tokenResponse = await msalClient.acquireTokenSilent({ account, scopes: [env.PUBLIC_AZURE_AD_B2C_API_SCOPE] }); request.headers.set('Authorization', `Bearer ${tokenResponse.accessToken}`); } } return fetch(request); };
服务端拦截
如果是服务端渲染时调用API,直接从event.locals或cookie中获取token添加到请求头即可。
内容的提问来源于stack exchange,提问作者crispanddry
相关产品推荐
相关产品推荐

