如何在NextAuth中实现登录后弹窗选择角色(含JWT策略与AzureADProvider场景)
如何在NextAuth中实现登录后弹窗选择角色(含JWT策略与AzureADProvider场景)
我之前刚好处理过类似的需求,结合NextAuth的JWT策略和AzureADProvider,给你梳理一套可直接复用的方案:
一、先配置AzureADProvider,确保拿到用户的角色列表
首先得从Azure AD那边把用户的角色列表捞出来,这一步是基础,配置的时候要注意scope和profile回调的处理:
// pages/api/auth/[...nextauth].js 或者 app/api/auth/[...nextauth]/route.js import NextAuth from "next-auth"; import AzureADProvider from "next-auth/providers/azure-ad"; export const authOptions = { // 必须用JWT策略,因为要在token里存选中的角色 session: { strategy: "jwt" }, providers: [ AzureADProvider({ clientId: process.env.AZURE_AD_CLIENT_ID, clientSecret: process.env.AZURE_AD_CLIENT_SECRET, tenantId: process.env.AZURE_AD_TENANT_ID, // 要包含能获取角色的scope,根据你的Azure AD配置调整 authorization: { params: { scope: "openid profile email roles", }, }, // 从Azure返回的profile中提取角色,注意键名可能是roles或者自定义claim profile(profile) { return { id: profile.sub, name: profile.name, email: profile.email, image: profile.picture, // 替换成你的实际角色claim键,比如自定义角色可能是profile["your-claim-uri/roles"] roles: profile.roles || profile["http://your-app-domain.com/roles"], }; }, }), ], callbacks: { // 把角色存入JWT token async jwt({ token, user }) { if (user) { token.roles = user.roles; // 初始化选中角色为null,标记还没选 token.selectedRole = token.selectedRole ?? null; } return token; }, // 把token中的数据同步到session,供客户端使用 async session({ session, token }) { session.user.roles = token.roles; session.user.selectedRole = token.selectedRole; return session; }, }, }; export default NextAuth(authOptions);
二、客户端实现登录后的角色选择弹窗
登录成功后,我们要在客户端判断用户是否已经选过角色,没选的话就弹出选择框。可以把这个逻辑放在全局布局或者_app.js里,确保所有页面都能触发:
// pages/_app.js 或者 app/layout.js(Next.js 13+) import { useSession, update } from "next-auth/react"; import { useState, useEffect } from "react"; export default function App({ Component, pageProps }) { const { data: session } = useSession(); const [showRoleModal, setShowRoleModal] = useState(false); useEffect(() => { // 登录成功、有角色列表、但没选过角色时,显示弹窗 if (session?.user?.roles?.length > 0 && !session.user.selectedRole) { setShowRoleModal(true); } }, [session]); const handleRoleSelect = async (selectedRole) => { // 调用后端API更新session中的角色 const res = await fetch("/api/auth/set-role", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ role: selectedRole }), }); if (res.ok) { // 更新客户端的session状态 await update(); setShowRoleModal(false); } else { alert("角色选择失败,请重试"); } }; return ( <> {/* 角色选择弹窗,样式可以根据你的项目UI调整 */} {showRoleModal && ( <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> <div className="bg-white p-6 rounded-lg w-96"> <h3 className="text-xl font-semibold mb-4">请选择你的角色</h3> <div className="space-y-2"> {session.user.roles.map((role) => ( <button key={role} className="w-full py-2 px-4 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors" onClick={() => handleRoleSelect(role)} > {role} </button> ))} </div> </div> </div> )} <Component {...pageProps} /> </> ); }
三、后端API更新JWT与Session
我们需要写一个专门的API来处理角色选择后的JWT更新,确保选中的角色能持久化到token中:
// pages/api/auth/set-role.js 或者 app/api/auth/set-role/route.js import { getServerSession } from "next-auth/next"; import { authOptions } from "../[...nextauth]"; import { getToken } from "next-auth/jwt"; import jwt from "jsonwebtoken"; export default async function handler(req, res) { if (req.method !== "POST") { return res.status(405).json({ error: "只支持POST请求" }); } // 验证用户是否已登录 const session = await getServerSession(req, res, authOptions); if (!session) { return res.status(401).json({ error: "未授权访问" }); } // 获取当前的JWT token const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET }); const { role } = req.body; // 验证选中的角色是否在用户的角色列表中,防止恶意篡改 if (token?.roles?.includes(role)) { // 更新token中的选中角色 const updatedToken = { ...token, selectedRole: role }; // 重新签名token,保持原有的过期时间 const signedToken = jwt.sign(updatedToken, process.env.NEXTAUTH_SECRET, { expiresIn: "30d", // 和你NextAuth配置的过期时间一致 }); // 更新浏览器中的session cookie res.setHeader("Set-Cookie", [ `next-auth.session-token=${signedToken}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=${30 * 24 * 60 * 60}`, ]); return res.status(200).json({ success: true }); } else { return res.status(400).json({ error: "无效的角色选择" }); } }
四、全局路由守卫,确保未选角色的用户不能访问受保护页面
最后,我们需要加一层路由守卫,防止用户跳过角色选择直接访问受保护路由。可以用NextAuth的middleware:
// middleware.ts(Next.js 13+ 项目根目录) import { withAuth } from "next-auth/middleware"; export default withAuth({ callbacks: { authorized({ token }) { // 只有选中角色的用户才能访问受保护路由 return !!token?.selectedRole; }, }, }); // 配置需要守卫的路由,根据你的项目调整 export const config = { matcher: ["/dashboard/:path*", "/admin/:path*", "/protected/:path*"], };
一些需要注意的细节
- 确保Azure AD中已经给用户分配了对应的角色,并且在应用注册的“Token配置”中添加了角色的声明,否则拿不到roles列表
- 如果你的项目用的是NextAuth的Database策略,逻辑类似,只是把选中的角色存在数据库的用户表中,而不是JWT里
- 弹窗的样式可以完全自定义,比如用MUI、Ant Design等组件库的Modal组件,我这里用的是Tailwind的基础样式
- 要处理角色列表为空的情况,比如给用户提示“你没有可用的角色,请联系管理员”
备注:内容来源于stack exchange,提问作者Guy Yannick
相关产品推荐
相关产品推荐

