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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 10:10:49