Next Auth刷新令牌触发多次回调问题求助
解决Next-Auth重复发送刷新令牌请求导致用户登出的问题
问题根源
- 前端重复触发session验证:Dashboard组件中同时使用了
useAuth(内部已调用useSession)和独立的useSession,导致Next-Auth多次触发session同步逻辑,进而多次调用JWT回调里的令牌刷新代码。 - 无并发请求控制:默认情况下,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
相关产品推荐
相关产品推荐

