如何使用NextAuth Credentials Provider刷新Firebase Access Token?
如何在NextAuth中自动刷新Firebase Access Token?
问题分析
你的现有代码仅在用户登录时获取一次Firebase Access Token,但未处理令牌过期后的自动刷新逻辑。Firebase ID Token默认有效期为1小时,过期后API请求会因认证失败返回错误。此外,当前代码未保存刷新令牌(refresh token)和令牌过期时间,无法触发自动刷新流程。
解决方案
要实现自动刷新,需要完成以下几个核心步骤:
- 登录时保存Firebase用户的
uid、refresh token以及Access Token的过期时间 - 在NextAuth的
jwt回调中检查令牌是否即将过期,若过期则调用Firebase的token刷新接口获取新令牌 - 更新
session回调,将最新的Access Token传递给前端 - 完善错误处理,让前端能感知刷新失败并引导用户重新登录
修改后的完整代码
import { auth } from "@/config/firebaseApp"; import { signInWithEmailAndPassword } from "firebase/auth"; import { AuthOptions, Session, User } from "next-auth"; import NextAuth from "next-auth/next"; import CredentialsProvider from "next-auth/providers/credentials"; // 定义自定义用户类型,避免any类型 interface FirebaseAuthUser extends User { uid: string; role: string; access_token: string; refresh_token: string; expires_at: number; } const authOptions: AuthOptions = { pages: { signIn: "/signIn", }, session: { strategy: "jwt", }, providers: [ CredentialsProvider({ type: "credentials", credentials: {}, authorize: async (credentials): Promise<FirebaseAuthUser | null> => { try { const { email, password } = credentials as { email: string; password: string }; const userCredentials = await signInWithEmailAndPassword(auth, email, password); const user = userCredentials.user; const accessToken = await user.getIdToken(); const refreshToken = user.refreshToken; // Firebase Token有效期为1小时,计算过期时间 const expiresAt = Date.now() + 3600 * 1000; return { uid: user.uid, role: "admin", // 这里可以根据实际业务逻辑设置角色 access_token: accessToken, refresh_token: refreshToken, expires_at: expiresAt, }; } catch (err: any) { // 抛出错误,让NextAuth捕获并提示用户登录失败 throw new Error(err.message); } }, }), ], callbacks: { async jwt({ token, user }) { // 用户登录时,初始化JWT数据 if (user) { const firebaseUser = user as FirebaseAuthUser; return { uid: firebaseUser.uid, role: firebaseUser.role, access_token: firebaseUser.access_token, refresh_token: firebaseUser.refresh_token, expires_at: firebaseUser.expires_at, }; } // 检查Token是否即将过期(提前1分钟刷新,避免刚好过期时请求失败) if (Date.now() < (token.expires_at as number) - 60 * 1000) { return token; } // Token过期,调用Firebase接口刷新 try { const response = await fetch( `https://securetoken.googleapis.com/v1/token?key=${process.env.NEXT_PUBLIC_FIREBASE_API_KEY}`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: token.refresh_token as string, }), } ); const data = await response.json(); if (!response.ok) throw data; return { ...token, access_token: data.id_token, // 如果返回新的refresh token则更新,否则保留原token refresh_token: data.refresh_token || token.refresh_token, expires_at: Date.now() + data.expires_in * 1000, }; } catch (error) { console.error("Token刷新失败:", error); // 刷新失败时添加错误标记,前端可据此引导用户重新登录 return { ...token, error: "RefreshTokenError" }; } }, async session({ session, token }) { if (session.user) { session.user.role = token.role as string; session.user.access_token = token.access_token as string; // 将错误信息传递到session,前端可监听处理 if (token.error) { session.error = token.error as string; } } return session; }, }, }; const handler = NextAuth(authOptions); export { handler as GET, handler as POST };
关键细节说明
- 保存必要信息:登录时不仅保存Access Token,还保存
refresh token和过期时间,这是自动刷新的基础 - Token过期检查:提前1分钟触发刷新,避免在请求API时刚好遇到Token过期的情况
- Firebase刷新接口:使用官方的Token刷新端点,传入refresh token获取新的ID Token
- 错误处理:刷新失败时在JWT中添加错误标记,前端可通过
useSession钩子监听该错误,引导用户重新登录 - 类型安全:定义自定义用户类型,避免使用
any,提升代码可维护性
前端使用提示
在前端调用API时,可通过useSession获取最新的access_token,并在遇到401错误或session中的error为RefreshTokenError时,引导用户重新登录或触发session刷新:
import { useSession } from "next-auth/react"; const { data: session, update } = useSession(); // 调用API时携带token const fetchData = async () => { try { const response = await fetch("/api/your-endpoint", { headers: { Authorization: `Bearer ${session?.user.access_token}`, }, }); if (response.status === 401 || session?.error === "RefreshTokenError") { // 触发session刷新,会调用jwt回调尝试刷新token await update(); // 刷新后重新请求 const newResponse = await fetch("/api/your-endpoint", { headers: { Authorization: `Bearer ${session?.user.access_token}`, }, }); // 处理新响应 } } catch (err) { // 处理错误 } };
内容的提问来源于stack exchange,提问作者user19921081
相关产品推荐
相关产品推荐

