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

Next.js中Azure AD Bearer Token存储与管理最佳实践问询

Next.js + NextAuth + Azure AD Bearer Token 管理最佳实践问题

我正在开发一个Next.js项目,已通过NextAuth实现邮箱/密码凭证认证。此外,我需要从Azure AD应用获取Bearer Token,以对该应用的各类端点发起认证请求。

关于在此场景下管理Azure AD Bearer Token的最佳实践,我有以下几个问题:

1. 用户专属Bearer Token

每个登录用户是否应拥有独立的Azure AD Bearer Token?若需要,在Next.js中(若可能结合NextAuth)的推荐处理方案是什么?

2. 服务端 vs 客户端存储

Azure AD Bearer Token应存储在服务端还是客户端?NextAuth能否无缝管理这类Token,或是Next.js应用中有安全存储与访问该Token的首选方案?


现有NextAuth配置代码(api/auth/[...nextauth]/route.ts)

import NextAuth, { NextAuthOptions } from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import prisma from '@/prisma/client';
import bcrypt from 'bcrypt';

export const authOptions: NextAuthOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    CredentialsProvider({
      name: 'credentials',
      credentials: {
        email: { label: 'Email', type: 'email', placeholder: 'Email'},
        password: { label: 'Password', type: 'password', placeholder: 'Password'}
      },
      async authorize(credentials, req) {
        if (!credentials?.email || !credentials.password) return null;
        
        const user = await prisma.user.findUnique({
          where: { email: credentials.email }
        });

        if (!user) return null;

        const passwordsMatch = await bcrypt.compare(
          credentials.password, 
          user.hashedPassword
        );

        return passwordsMatch ? user : null;
      }
    })
  ]
}

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST }

获取Token的示例代码(不确定放置位置及与Next.js应用的关联方式)

var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
myHeaders.append("Cookie", "fpc=ArnnFhTMCf1OmHBMGIeu_bbGVvFcAQAAAGZSCt0OAAAA");

var urlencoded = new URLSearchParams();
urlencoded.append("grant_type", "client_credentials");
urlencoded.append("client_id", process.env.CLIENT_ID);
urlencoded.append("client_secret", process.env.CLIENT_SECRET);
urlencoded.append("scope", "https://api.businesscentral.dynamics.com/.default");

var requestOptions = {
  method: 'POST',
  headers: myHeaders,
  body: urlencoded,
  redirect: 'follow'
};

fetch("https://login.microsoftonline.com/[TENANT_ID]/oauth2/v2.0/token", requestOptions)
  .then(response => response.text())
  .then(result => console.log(result))
  .catch(error => console.log('error', error));

问题解答

1. 用户专属Bearer Token

是否需要用户专属Token,取决于你的Azure AD API需求:

  • 如果API需要识别具体用户身份(比如关联用户数据、基于用户权限返回结果),必须为每个用户生成独立的Bearer Token;
  • 如果只是调用无用户上下文的通用接口,用client_credentials模式的应用级Token即可(你的示例代码就是这种模式,Token和用户无关)。

结合NextAuth的处理方案(针对需要用户专属Token的场景):
由于你用的是自定义邮箱密码体系,需要将用户和Azure AD关联,推荐在用户登录后,通过**ROPC流(Resource Owner Password Credentials Flow,仅限内部可信应用)**获取用户专属Token:

  1. 在authorize函数中,验证用户邮箱密码通过后,调用Azure AD的Token端点,传入用户的邮箱密码获取Access Token和Refresh Token;
  2. 将Token通过NextAuth的JWT机制存储,或存入用户数据库记录;
  3. 后续调用Azure API时,从服务端取出该用户的Token使用。

示例修改:在authorize函数中添加获取用户Token的逻辑

async authorize(credentials, req) {
  if (!credentials?.email || !credentials.password) return null;
  
  const user = await prisma.user.findUnique({
    where: { email: credentials.email }
  });

  if (!user) return null;

  const passwordsMatch = await bcrypt.compare(
    credentials.password, 
    user.hashedPassword
  );

  if (!passwordsMatch) return null;

  // 获取用户专属Azure AD Token(ROPC流)
  const urlencoded = new URLSearchParams();
  urlencoded.append("grant_type", "password");
  urlencoded.append("client_id", process.env.AZURE_CLIENT_ID);
  urlencoded.append("client_secret", process.env.AZURE_CLIENT_SECRET);
  urlencoded.append("scope", "https://api.businesscentral.dynamics.com/.default");
  urlencoded.append("username", credentials.email);
  urlencoded.append("password", credentials.password);
  urlencoded.append("tenant", process.env.AZURE_TENANT_ID);

  const tokenRes = await fetch(`https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/oauth2/v2.0/token`, {
    method: 'POST',
    body: urlencoded,
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
  });

  const tokenData = await tokenRes.json();
  if (!tokenRes.ok) return null;

  // 返回用户数据时携带Token,后续存入JWT
  return {
    ...user,
    azureAccessToken: tokenData.access_token,
    azureRefreshToken: tokenData.refresh_token,
    azureExpiresAt: Date.now() + tokenData.expires_in * 1000
  };
}

2. 服务端 vs 客户端存储

绝对不要将Azure AD Bearer Token存在客户端(localStorage、sessionStorage甚至普通Cookie),这类存储方式容易被XSS攻击窃取,导致API权限泄露。

最佳存储方案:

  1. 服务端数据库存储:在用户表中新增azureAccessToken、azureRefreshToken、azureExpiresAt字段,将Token关联到用户记录,每次调用API时从服务端查询获取;
  2. NextAuth加密JWT存储:通过NextAuth的jwt回调将Token存入加密的JWT中,JWT会以HttpOnly、Secure的Cookie形式存在客户端,无法被前端JS读取,安全性较高。

NextAuth可以无缝管理这类Token,示例配置:

export const authOptions: NextAuthOptions = {
  // ... 现有配置
  session: { strategy: "jwt" }, // 必须设置为jwt模式才能自定义存储内容
  callbacks: {
    async jwt({ token, user }) {
      // 用户登录时,将Azure Token存入JWT
      if (user) {
        token.azureAccessToken = user.azureAccessToken;
        token.azureRefreshToken = user.azureRefreshToken;
        token.azureExpiresAt = user.azureExpiresAt;
      }

      // 检查Token是否过期,自动刷新
      if (Date.now() > token.azureExpiresAt) {
        const urlencoded = new URLSearchParams();
        urlencoded.append("grant_type", "refresh_token");
        urlencoded.append("client_id", process.env.AZURE_CLIENT_ID);
        urlencoded.append("client_secret", process.env.AZURE_CLIENT_SECRET);
        urlencoded.append("refresh_token", token.azureRefreshToken);
        urlencoded.append("scope", "https://api.businesscentral.dynamics.com/.default");

        const refreshRes = await fetch(`https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/oauth2/v2.0/token`, {
          method: 'POST',
          body: urlencoded,
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
        });

        const refreshData = await refreshRes.json();
        if (refreshRes.ok) {
          token.azureAccessToken = refreshData.access_token;
          token.azureRefreshToken = refreshData.refresh_token;
          token.azureExpiresAt = Date.now() + refreshData.expires_in * 1000;
        }
      }

      return token;
    },
    async session({ session, token }) {
      // 注意:不要将敏感Token返回给客户端,若仅服务端使用可省略此步骤
      // 若需客户端调用API(不推荐,建议通过Next.js API路由代理),可按需添加
      session.user.azureAccessToken = token.azureAccessToken;
      return session;
    }
  }
}

调用Azure API时,推荐通过Next.js的API路由代理,在服务端获取Token并发起请求,避免客户端直接接触Token:

// app/api/azure-proxy/route.ts
import { getServerSession } from "next-auth/next";
import { authOptions } from "../auth/[...nextauth]/route";

export async function GET() {
  const session = await getServerSession(authOptions);
  if (!session?.user.azureAccessToken) {
    return new Response("Unauthorized", { status: 401 });
  }

  const res = await fetch("https://api.businesscentral.dynamics.com/your-endpoint", {
    headers: {
      Authorization: `Bearer ${session.user.azureAccessToken}`
    }
  });

  const data = await res.json();
  return Response.json(data);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 18:10:57