Next.js中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:
- 在
authorize函数中,验证用户邮箱密码通过后,调用Azure AD的Token端点,传入用户的邮箱密码获取Access Token和Refresh Token; - 将Token通过NextAuth的JWT机制存储,或存入用户数据库记录;
- 后续调用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权限泄露。
最佳存储方案:
- 服务端数据库存储:在用户表中新增
azureAccessToken、azureRefreshToken、azureExpiresAt字段,将Token关联到用户记录,每次调用API时从服务端查询获取; - 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

