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

Next Auth刷新令牌触发多次回调问题求助

解决Next-Auth重复发送刷新令牌请求导致用户登出的问题

问题根源

  1. 前端重复触发session验证:Dashboard组件中同时使用了useAuth(内部已调用useSession)和独立的useSession,导致Next-Auth多次触发session同步逻辑,进而多次调用JWT回调里的令牌刷新代码。
  2. 无并发请求控制:默认情况下,Next-Auth的JWT回调没有锁机制,当多个请求同时触发令牌刷新时,会向后端发送多份刷新请求。后端执行令牌轮换后,旧令牌失效,后续请求携带的旧令牌会返回400错误,这个错误状态会覆盖成功刷新的令牌数据,最终触发用户登出。

解决方案

1. 移除重复的useSession调用

修改Dashboard.js,去掉独立的useSession调用,改为从useAuth中获取会话数据,减少不必要的session同步触发:

const Dashboard = () => {
  const { isAuthenticated, session } = useAuth(true);

  return (
    <>
      {isAuthenticated ? (
        <Layout title="Dashboard" content="Members Area Dashboard">
          <div>
            <h2>Welcome {session?.user.username} to the Members Area!</h2>
          </div>
        </Layout>
      ) : (
        <div>
          <p>Error: User Signed Out! Login to access this page.</p>
          <p>
            <Link href="/members/login">Login</Link>
          </p>
        </div>
      )}
    </>
  );
};

export default Dashboard;

对应修改useAuth.js,让它同时返回认证状态和会话数据:

export default function useAuth(shouldRedirect) {
  const { data: session } = useSession();
  const router = useRouter();
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  useEffect(() => {
    if (session?.error === "RefreshAccessTokenError") {
      signOut();
    }

    if (session === null) {
      if (router.route !== "/members/login") {
        router.replace("/members/login");
      }
      setIsAuthenticated(false);
    } else if (session !== undefined) {
      if (router.route === "/members/login") {
        router.replace("/members/dashboard");
      }
      setIsAuthenticated(true);
    }
  }, [session]);

  return { isAuthenticated, session };
}

2. 给刷新令牌请求加并发锁

在[...nextauth].js中实现简单的锁机制,确保同一时间只有一个刷新请求在执行,避免重复发送:

import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { API_URL } from "@/constants/constants";
import { signOut } from "next-auth/react";

// 全局变量存储正在进行的刷新请求,实现并发锁
let refreshTokenPromise = null;

async function refreshAccessToken(tokenObject) {
  // 如果已有刷新请求在处理,直接等待其完成
  if (refreshTokenPromise) {
    return refreshTokenPromise;
  }

  try {
    refreshTokenPromise = (async () => {
      const tokenData = {
        refreshToken: tokenObject.refreshToken,
        userName: tokenObject.username,
      };

      const data = JSON.stringify(tokenData);
      const tokenResponse = await fetch(`${API_URL}/refresh-token`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Accept: "application/json",
        },
        body: data,
      });
      const response = await tokenResponse.json();

      if (tokenResponse.status === 200) {
        return {
          ...tokenObject,
          accessToken: response.accessToken,
          accessTokenExpiry: response.accessTokenExpiry,
          refreshToken: response.refreshToken,
          refreshTokenExpiry: response.refreshTokenExpiry,
          error: undefined,
        };
      } else {
        return {
          ...tokenObject,
          error: "RefreshAccessTokenError",
        };
      }
    })();

    return await refreshTokenPromise;
  } catch (error) {
    return {
      ...tokenObject,
      error: "RefreshAccessTokenError",
    };
  } finally {
    // 请求完成后重置锁,允许后续刷新请求执行
    refreshTokenPromise = null;
  }
}

// 以下为原代码修正(修复拼写错误和逻辑漏洞)
const providers = [
  CredentialsProvider({
    async authorize(credentials, req) {
      const { Username, Password } = credentials;

      const body = JSON.stringify({
        Username,
        Password,
      });

      const res = await fetch(`${API_URL}/Login`, {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json; charset=utf-8",
        },
        body: body,
      }).catch((error) => {
        const fetchError = {
          success: false,
          message: "Sorry, something is wrong with our servers...",
        };
        return fetchError;
      });

      let data = "";
      if (res.ok) {
        data = await res.json();
      } else if (res.status === 400 || res.status === 500) {
        data = await res.json();
      } else {
        data = res;
      }

      // 修正拼写错误:dataaccessToken → data.accessToken
      const user = {
        success: data.success,
        message: data.message,
        id: data.id,
        email: data.email,
        username: data.username,
        accessToken: data.accessToken,
        accessTokenExpiry: data.accessTokenExpiry,
        refreshToken: data.refreshToken,
        refreshTokenExpiry: data.refreshTokenExpiry,
        roles: data.roles,
      };

      if (res.ok && user) {
        return user;
      } else {
        return user;
      }
    },
  }),
];

const callbacks = {
  async signIn({ user }) {
    if (user.success) {
      return true;
    } else {
      let url = `/members/login?error=${user.message}`;
      return url;
    }
  },
  async jwt({ token, user }) {
    if (user) {
      token.accessToken = user.accessToken;
      token.accessTokenExpiry = user.accessTokenExpiry;
      token.refreshToken = user.refreshToken;
      token.refreshTokenExpiry = user.refreshTokenExpiry;
      token.username = user.username;
      token.user = user;
      token.roles = user.roles;
      token.error = undefined;
    }

    let utcDateNow = new Date().toISOString();

    if (token.accessTokenExpiry > utcDateNow) {
      return token;
    }

    token = await refreshAccessToken(token);

    if (token?.error === "RefreshAccessTokenError") {
      signOut();
    }

    return token;
  },
  async session({ session, token }) {
    session.user = token.user;
    session.accessToken = token.accessToken;
    session.accessTokenExpiry = token.accessTokenExpiry;
    session.refreshToken = token.refreshToken;
    session.refreshTokenExpiry = token.refreshTokenExpiry;
    session.error = token.error;

    return session;
  },
};

export const options = {
  providers,
  callbacks,
  pages: {
    signIn: "/members/login",
    signOut: "/members/signout",
    error: "/members/auth-error",
  },
};

const Auth = (req, res) => NextAuth(req, res, options);
export default Auth;

3. 额外说明

  • 并发锁通过全局变量refreshTokenPromise实现,确保同一时间只有一个刷新请求发送到后端,避免令牌轮换后的无效请求。
  • 修复了原代码中的拼写错误和未定义变量问题,这些问题可能导致刷新逻辑异常,加剧重复请求的影响。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 02:52:04