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

NextAuth对接Azure AD:令牌过期识别异常及流程排查

问题诊断与修复方案

核心问题在于你的GateKeeper组件令牌过期检测的执行时机错误,且未充分利用NextAuth内置的session更新机制,导致客户端无法及时感知令牌过期状态,进而出现页面正常加载但API请求401的情况。

问题点拆解

  1. 同步执行登出逻辑失效:在组件渲染阶段直接调用signOut(),此时session状态可能未完成更新,还会引发不必要的重复渲染,最终无法正确触发过期后的处理流程。
  2. 无主动session状态刷新:useSession默认不会主动轮询session状态,令牌过期后客户端无法自动感知,必须等待页面刷新或手动触发更新。
  3. 逻辑顺序错误:在loading状态未结束时就执行过期判断,可能读取到旧的session数据。

修复方案

1. 重构GateKeeper组件逻辑

将过期检测移至useEffect中,确保仅在session状态变化时执行,同时优先处理加载状态,避免无效判断:

//GateKeeper.js
import { useSession, signIn, signOut } from 'next-auth/react'
import { useEffect } from 'react'
import SignIn from '@components/auth/SignIn'

const GateKeeper = (props) => {
  const { data: session, status, update } = useSession({
    // 可选:设置定期刷新session,每30秒检查一次状态
    refetchInterval: 30 * 1000
  })

  useEffect(() => {
    // 处理刷新令牌失败的情况
    if (session?.error === "RefreshAccessTokenError") {
      signOut()
      return
    }

    // 检测令牌是否过期
    if (session?.accessTokenExpires) {
      const isExpired = Date.now() > session.accessTokenExpires
      if (isExpired) {
        // 先触发session更新,让jwt回调执行刷新逻辑
        update().then(() => {
          // 更新后仍过期则直接登出
          if (Date.now() > session.accessTokenExpires) {
            signOut()
          }
        })
      }
    }
  }, [session, status, update])

  if (status === "loading") {
    return null // 加载状态下不渲染内容
  }

  if (status === "unauthenticated" || !session) {
    return <SignIn />
  }

  return <>{props.children}</>
}

export default GateKeeper

2. 优化NextAuth回调逻辑

确保jwt和session回调的字段映射准确,同时兼容Azure AD返回的时间格式:

// [...nextauth].js
import NextAuth from "next-auth"
import AzureADProvider from "next-auth/providers/azure-ad"

async function refreshAccessToken(token) {
  try {
    const url =
      `https://login.microsoftonline.com/${process.env.AZURE_AD_TENANT_ID}/oauth2/v2.0/token` +
      new URLSearchParams({
        client_id: process.env.AZURE_AD_CLIENT_ID,
        client_secret: process.env.AZURE_AD_CLIENT_SECRET,
        scope: 'email openid profile User.Read offline_access',
        grant_type: 'refresh_token',
        refresh_token: token.refreshToken,
      })

    const response = await fetch(url, {
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      method: "POST",
    })

    const refreshedTokens = await response.json()

    if (!response.ok) {
      throw refreshedTokens
    }

    return {
      ...token,
      accessToken: refreshedTokens.id_token,
      // 确保时间戳转成毫秒格式,兼容客户端判断
      accessTokenExpires: (refreshedTokens.expires_at || Date.now() / 1000 + 3600) * 1000,
      refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
    }
  } catch (error) {
    console.log(error)
    return {
      ...token,
      error: "RefreshAccessTokenError",
    }
  }
}

export default NextAuth({
  providers: [
    AzureADProvider({
      clientId: process.env.AZURE_AD_CLIENT_ID,
      clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
      tenantId: process.env.AZURE_AD_TENANT_ID,
      authorization: { params: { scope: 'email openid profile User.Read offline_access' } }
    })
  ],
  callbacks: {
    async signIn() {
      return true
    },
    async jwt({ token, user, account }) {
      // 首次登录初始化token数据
      if (account && user) {
        return {
          accessToken: account.id_token,
          accessTokenExpires: account.expires_at * 1000,
          refreshToken: account.refresh_token,
          user,
        }
      }

      // 令牌未过期则返回原token
      if (Date.now() < token.accessTokenExpires) {
        return token
      }

      // 令牌过期,触发刷新逻辑
      return refreshAccessToken(token)
    },
    async session({ session, token }) {
      // 简化客户端获取字段的路径
      session.user = token.user
      session.accessToken = token.accessToken
      session.accessTokenExpires = token.accessTokenExpires
      session.error = token.error

      return session
    },
    async redirect({ url, baseUrl }) {
      return baseUrl
    }
  }
})

额外优化建议

  • API请求拦截:在全局API请求拦截器中捕获401错误,主动调用update()方法刷新session,若刷新失败直接跳转登录。
  • 弱化客户端时间依赖:客户端时间可能存在偏差,优先依赖NextAuth服务器端的jwt回调处理过期逻辑,客户端仅做补充校验。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 21:55:25