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

如何使用MSAL实现前端无需用户登录的API身份验证

问题描述

我有一个已在Azure中注册的应用,该应用提供用户登录功能,但使用应用本身无需登录。目前我们使用Microsoft Entra和MSAL React包处理用户的登录/登出及账户管理。

我们需要对应用自身进行认证,使得前端向.NET后端发起的每个API请求都携带Bearer令牌。

我尝试使用授权码流来实现,但获取访问令牌所需的授权码会作为参数通过重定向URI返回,这需要用户交互才能提取授权码并调用第二个端点获取实际令牌以保障API请求安全。

请问MSAL是否支持我想要实现的场景?我是否需要使用客户端凭证流(守护程序推荐的认证流),还是该功能应由后端实现?


现有代码片段

MSAL配置与AuthProvider

import { PublicClientApplication } from '@azure/msal-browser';

export const msalConfig = {
  auth: {
    authority: "https://login.microsoftonline.com/TenantId",
    clientId: "ClientId",
    postLogoutRedirectUri: 'http://localhost:3000',
    redirectUri: 'http://localhost:3000',
    validateAuthority: true,
    navigateToLoginRequestUrl: true,
  },
  cache:
  {
    cacheLocation: 'sessionStorage',
    storeAuthStateInCookie: true,
  }
}

export const msalInstance = new PublicClientApplication(msalConfig);

interface AuthProviderProps {
    children : React.ReactNode
}

function AuthProvider ({ children } : AuthProviderProps) {
    return <MsalProvider instance={msalInstance}>
        {children}
    </MsalProvider>
}

export default AuthProvider;

应用布局(layout.tsx)

<AuthProvider>
    <ThemeProvider theme={theme}>
      <Providers>
        <Header />
        <main className="lg:!pb-10 sm:pb-2 lg:pb-5 sm:pt-[4rem] lg:pt-0 sm:min-h-[calc(100%-6%)] lg:min-h-auto">
          {children}
          <MuiXLicense />
        </main>
        <Footer />
        <CustomToast />
        <Loader />
      </Providers>
    </ThemeProvider>
</AuthProvider>

Next.js API路由(/api/secure-api)

export async function POST() {
  try {
    const params = new URLSearchParams({
      client_id: 'ClientId',
      scope: 'api://TenantId/.default',
      response_type: "code",
      client_secret: 'ClientSecret',
      code_challenge_method: 'S256',
      code_challenge: 'Nj9Youq443xUOCe_HsmBXJy5dKC8YsqlUdn1sga3CR0',
      redirect_uri: "http://localhost:3000",
      response_mode: "query",
      state: '12345'
    })
    const authorize = await fetch(`https://login.microsoftonline.com/TenantId/oauth2/v2.0/authorize?${params.toString()}`, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
      },
    });
    console.log('authorize: ,', authorize, authorize.body);
    const data = await authorize.json();
    const code = data.data.refresh_token;
    if (!authorize.ok) {
      return NextResponse.json(data, { status: authorize.status });
    }
    const accessTokenParams = new URLSearchParams({
      grant_type: "client_credentials",
      redirect_uri: "https://dev-travelinsured.azurewebsites.net",
      client_id: 'ClientID',
      scope: 'api://Instance/.default',
      client_secret: 'ClientSecret',
      code_verifier: 'Nj9Youq443xUOCe_HsmBXJy5dKC8YsqlUdn1sga3CR0',
      code: code,
    })
    const response = await fetch(
      `https://login.microsoftonline.com/tenantId/oauth2/v2.0/token?${accessTokenParams.toString()}`, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept',
          'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
        },
      });
    console.log(response);

    const accessToken = await response.json();
    if (!response.ok) {
      return NextResponse.json(accessToken, { status: response.status });
    }

    return NextResponse.json(accessToken, { status: 200 });
  } catch (error) {
    console.error("Error in POST /api/secure-api", error);
    return NextResponse.json({ error: error }, { status: 500 });
  }
}

RTK Query令牌管理

export const generateSecureApiToken = createApi({
  reducerPath: "generateSecureApiToken",
  baseQuery: fetchBaseQuery({ baseUrl: "/api/secure-api" }),
  endpoints: (builder) => ({
    getAccessToken: builder.query<any, void>({
      query: () => ({
        url: '',
        method: "POST",
      }),
      async onQueryStarted(_, { dispatch, queryFulfilled }) {
        try {
          const { data } = await queryFulfilled;
          if (data && data.access_token) {
            const tokenData = {
              accessToken: data.access_token,
              expiresIn: data.expires_in,
            };

            localStorage.setItem("apiAccessToken", data.access_token);
            localStorage.setItem("apiExpiresIn", data.expires_in.toString());
            console.log('tokenData: ,', tokenData);
            dispatch(setAccessToken(tokenData));
          } else {
            console.error("No access token found in the response:", data);
          }
        } catch (error) {
          console.error("Failed to fetch access token:", error);
        }
      },
    }),
  }),
});

const baseQuery = fetchBaseQuery({
  baseUrl: "/api/secure-api",
  prepareHeaders: (headers, { getState }) => {
    const token = (getState() as RootState).apiAuth.accessToken;
    if (token) {
      headers.set('Authorization', `Bearer ${token}`);
    }
    return headers;
  },
});

export const baseQueryWithReauth: typeof baseQuery = async (args, api, extraOptions) => {
  let result = await baseQuery(args, api, extraOptions);

  if (result.error && result.error.status === 401) {
    // Trigger the token refresh
    const refreshResult = await api.dispatch(
      generateSecureApiToken.endpoints.getAccessToken.initiate()
    ).unwrap();

    if (refreshResult && refreshResult.access_token) {
      // Store the new token in Redux and LocalStorage
      api.dispatch(setAccessToken({ accessToken: refreshResult.access_token, expiresIn: refreshResult.expires_in }));
      localStorage.setItem('apiAccessToken', refreshResult.access_token);
      localStorage.setItem('apiExpiresIn', refreshResult.expires_in.toString());

      // Retry the original query with the new token
      result = await baseQuery(args, api, extraOptions);
    } else {
      // Handle token refresh failure (optional)
      console.error("Failed to refresh access token.");
    }
  }

  return result;
};

解决方案

场景适配性分析

你的核心需求是无需用户登录的情况下,前端能安全获取令牌调用后端API,授权码流完全不适合这个场景——它本质是用户-centric的流程,必须依赖用户交互完成授权。

正确技术选择

  1. 客户端凭证流是唯一正确方向:
    这个流专门用于服务对服务的认证,不需要用户参与,完全依赖应用自身的身份(Client ID + Client Secret/证书)获取令牌。但要注意:绝对不能在前端代码中暴露Client Secret,前端是公开环境,Secret会被轻易窃取,导致应用身份泄露。

  2. 令牌获取逻辑必须放在后端:
    应该在你的.NET后端或中间层(比如当前用的Next.js API路由)中实现客户端凭证流的令牌获取逻辑,前端只需要调用这个后端接口拿到令牌,再携带令牌调用业务API。这样既能保证Client Secret的安全性,也能统一管理令牌的缓存和刷新。

代码修正建议

  1. 修正Next.js API路由逻辑:
    你当前的代码混淆了授权码流和客户端凭证流的参数,以下是正确的客户端凭证流实现:

    // /api/secure-api
    export async function POST() {
      try {
        const params = new URLSearchParams({
          grant_type: "client_credentials",
          client_id: '你的ClientId',
          scope: 'api://你的后端API应用ID/.default', // 注意:这里是后端API的应用ID,不是租户ID
          client_secret: '你的ClientSecret'
        });
    
        const response = await fetch(
          `https://login.microsoftonline.com/你的TenantId/oauth2/v2.0/token`,
          {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: params.toString()
          }
        );
    
        const accessToken = await response.json();
        if (!response.ok) {
          return NextResponse.json(accessToken, { status: response.status });
        }
    
        return NextResponse.json(accessToken, { status: 200 });
      } catch (error) {
        console.error("获取令牌失败", error);
        return NextResponse.json({ error: "获取令牌失败" }, { status: 500 });
      }
    }
    
    • 请求方法必须为POST,Content-Type需设置为application/x-www-form-urlencoded,这是OAuth2令牌端点的强制要求。
    • Scope格式必须是api://{后端API应用ID}/.default,表示使用应用级权限。
  2. MSAL角色调整:
    当前的MSAL配置是用于用户登录的,可以保留(用于用户账户管理功能),但它和API认证的令牌获取逻辑完全分离——API认证的令牌不需要通过MSAL获取,直接调用后端的令牌接口即可。

  3. 令牌缓存优化:

    • 在后端层面缓存令牌(比如用Redis),避免每次请求都调用Entra的令牌端点,提升性能并减少请求次数。
    • 前端可以将令牌缓存到内存或localStorage,但要监听令牌过期时间,过期后重新调用后端接口获取新令牌。

Azure权限配置要求

  • 确保你的前端/中间层应用被授予了后端API的应用权限(不是委派权限),且该权限已被管理员同意。
  • 后端API的应用注册中,需暴露对应的API范围,并在权限配置中添加前端/中间层应用的访问权限。

内容的提问来源于stack exchange,提问作者kristin1111

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 17:05:53