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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.15 15:34:35