NextAuth+Firebase邮箱认证:onAuthStateChanged前后端状态不一致求助
NextAuth + Firebase 邮箱认证前后端用户状态不一致问题修复指导
我正在结合NextAuth与Firebase实现认证功能,包含谷歌认证和Firebase邮箱认证两种方式。在实现邮箱认证时,发现onAuthStateChanged返回的用户状态在前后端不一致:客户端中user值为null,显示未登录;但在getServerSideProps函数中user值非空,可访问email属性,显示已登录。我通过authOptions中的authorize方法完成用户登录,恳请提供修复该问题的指导。
主页面(/main)代码
import HomeHeader from "@/components/HomeHeader"; import Feed from "@/components/Feed"; import ProfileSidebar from "@/components/ProfileSidebar"; import UserSuggestions from "@/components/UserSuggestions"; import { Flex, Spacer, Button } from "@chakra-ui/react"; import { motion } from "framer-motion"; import _get from "lodash/get"; import { createContext, useState } from "react"; import { signOut, getCsrfToken, getProviders } from "next-auth/react"; import { getServerSession } from "next-auth"; import { authOptions } from "./api/auth/[...nextauth]"; import { auth, firestore } from "@firebase/firebase"; import { doc, getDoc } from "firebase/firestore"; import { useRouter } from "next/router"; import { signOut as signOutFirebase, onAuthStateChanged } from "firebase/auth"; export const UserContext = createContext(); export default function Main({ session }) { const [user, setUser] = useState(_get(session, "user", {})); const router = useRouter(); onAuthStateChanged(auth, (user) => { if (user) { console.log(user.email); } else { console.log("No user"); // client side prints no user } }); const signOutCustom = () => { if (auth.currentUser) { signOutFirebase(auth) .then(() => { router.push("/"); }) .catch((error) => { console.log(error); }); } else { signOut(); } }; return ( <UserContext.Provider value={{ user, setUser }}> <motion.div initial={{ opacity: 0 }} whileInView={{ opacity: 1 }} viewport={{ once: true }} transition={{ duration: 0.5 }} animate={{ opacity: 1 }} > <div className="h-screen w-screen"> <HomeHeader /> <div className="mx-auto max-h-screen xl:max-w-7xl mt-8 max-w-6xl overflow-y"> <Flex gap={4}> <ProfileSidebar /> <Spacer /> <Feed /> <Spacer /> <UserSuggestions /> <Button onClick={signOutCustom}>Sign Out</Button> </Flex> </div> </div> </motion.div> </UserContext.Provider> ); } export async function getServerSideProps(context) { const session = await getServerSession(context.req, context.res, authOptions); const isSignedIn = auth.currentUser; onAuthStateChanged(auth, (user) => { if (user) { console.log(user.email); // this would print email } else { console.log("No user"); } }); if (!session && !isSignedIn) { return { redirect: { destination: "/", permanent: false, }, }; } if (!session && isSignedIn) { const userId = auth.currentUser.uid; const userDoc = await getDoc(doc(firestore, "users", userId)); if (userDoc.exists()) { const user = userDoc.data(); return { props: { session: { user: { ...user, uid: userId, }, }, }, }; } else { return { redirect: { destination: "/", permanent: false, }, }; } } return { props: { session: session, }, }; }
[...nextauth].js代码
import NextAuth from "next-auth"; import GoogleProvider from "next-auth/providers/google"; import { FirestoreAdapter } from "@next-auth/firebase-adapter"; import { cert } from "firebase-admin/app"; import admin from "firebase-admin"; import { signInWithCustomToken, signInWithEmailAndPassword, onAuthStateChanged} from "firebase/auth"; import { auth, firestore } from "@/firebase/firebase"; import CredentialsProvider from "next-auth/providers/credentials"; import { doc, getDoc } from "firebase/firestore"; export const authOptions = { adapter: FirestoreAdapter({ credential: cert({ projectId: process.env.FIREBASE_PROJECT_ID, clientEmail: process.env.FIREBASE_CLIENT_EMAIL, privateKey: process.env.FIREBASE_PRIVATE_KEY, }) }), providers: [ CredentialsProvider({ name: "Credentials", credentials: { email: { label: 'Email', type: 'text', }, password: { label: 'Password', type: 'password', }, }, async authorize(credentials, req) { try{ const userCredential = await signInWithEmailAndPassword(auth, credentials.email, credentials.password); const userId = userCredential.user.uid; const userDoc = await getDoc(doc(firestore, "users", userId)); if (userDoc.exists()){ const user = userDoc.data(); const userId = userDoc.id; user.uid = userId; return user; } else{ throw new Error("User not found"); } } catch (error) { throw new Error(error); } } }), GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), ], callbacks: { async signIn({ user, account, profile, email, credentials }) { if (user) return true; else return false; }, async jwt({token, user, account, profile, isNewUser}) { return { user: { uid: user.uid, email: user.email, image: user.image, name: user.firstName + " " + user.lastName, } } }, async session({ session, user, token }) { try { let isSignedIn = false; onAuthStateChanged(auth, (credentialUser) => { if (credentialUser) { isSignedIn = true; } }); if (isSignedIn) return session; await admin.auth().getUserByEmail(user.email); const customToken = await admin.auth().createCustomToken(user.id); await signInWithCustomToken(auth, customToken).then((userCredential) => { const user_ = userCredential.user; } ).catch((error) => { throw new Error(error); }) return session; } catch (error) { if (error.code === "auth/user-not-found") { await admin.auth().createUser({ uid: user.id, email: user.email, emailVerified: false, image: user.image, providerData: [ { providerId: "google.com", uid: user.id, email: user.email, displayName: user.name, photoURL: user.image, }, ], }); const customToken = await admin.auth().createCustomToken(user.id); await signInWithCustomToken(auth, customToken).then((userCredential) => { const user = userCredential.user; }).catch((error) => { throw new Error(error); }) return session; } } }, }, secret: process.env.NEXTAUTH_SECRET, debug: true, } export default NextAuth(authOptions);
问题原因与修复方案
核心问题分析
- 客户端Firebase Auth未同步登录状态:CredentialsProvider的
authorize方法中使用了客户端Firebase Auth的signInWithEmailAndPassword,该登录状态仅存在于服务器端Node.js环境,不会自动同步到浏览器客户端。 - 服务器端误用客户端Firebase Auth实例:
getServerSideProps中直接调用客户端auth.currentUser,服务器端是无状态的,每个请求独立,导致状态混乱。 - Session回调中
onAuthStateChanged无效:onAuthStateChanged是异步监听,在同步的session回调中无法正确获取状态,逻辑判断失效。
具体修复步骤
1. 重构CredentialsProvider的authorize方法(服务器端处理)
不要在authorize中使用客户端Firebase Auth,改用Firebase Admin SDK验证用户凭证,同时调整前端登录逻辑:
// [...nextauth].js中修改CredentialsProvider的authorize方法 async authorize(credentials, req) { try { // 验证客户端传来的idToken const decodedToken = await admin.auth().verifyIdToken(credentials.idToken); const userId = decodedToken.uid; const userDoc = await getDoc(doc(firestore, "users", userId)); if (userDoc.exists()) { const user = userDoc.data(); user.uid = userId; user.email = decodedToken.email; return user; } else { throw new Error("User not found"); } } catch (error) { throw new Error(error.message); } } // 前端登录逻辑调整 import { signInWithEmailAndPassword } from "firebase/auth"; import { signIn } from "next-auth/react"; const handleLogin = async (email, password) => { try { const userCredential = await signInWithEmailAndPassword(auth, email, password); const idToken = await userCredential.user.getIdToken(); // 调用NextAuth的Credentials登录 await signIn("credentials", { idToken: idToken, redirect: true, callbackUrl: "/main" }); } catch (error) { console.error(error); } };
2. 修复getServerSideProps中的状态判断
服务器端完全依赖NextAuth的getServerSession,移除客户端Firebase Auth实例的调用:
export async function getServerSideProps(context) { const session = await getServerSession(context.req, context.res, authOptions); if (!session) { return { redirect: { destination: "/", permanent: false, }, }; } // 从Firestore获取额外用户信息 const userDoc = await getDoc(doc(firestore, "users", session.user.uid)); if (userDoc.exists()) { return { props: { session: { ...session, user: { ...session.user, ...userDoc.data() } } } }; } else { return { redirect: { destination: "/", permanent: false, }, }; } }
3. 客户端同步Firebase Auth状态
使用NextAuth的useSession钩子监听登录状态,通过自定义API路由获取Firebase自定义token,同步客户端登录状态:
// 主页面中修改状态同步逻辑 import { useSession } from "next-auth/react"; import { useEffect } from "react"; import { signInWithCustomToken } from "firebase/auth"; import { auth } from "@firebase/firebase"; export default function Main({ session }) { const { data: sessionData } = useSession(); const [user, setUser] = useState(sessionData?.user || {}); const router = useRouter(); useEffect(() => { if (sessionData?.user) { // 请求自定义token同步Firebase状态 fetch("/api/auth/firebase-custom-token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uid: sessionData.user.uid }) }) .then(res => res.json()) .then(data => { if (data.token) { signInWithCustomToken(auth, data.token) .then(userCredential => { setUser(userCredential.user); }); } }); } else { // 登出时同步Firebase状态 if (auth.currentUser) { signOutFirebase(auth); } setUser({}); } }, [sessionData]); // 其余代码保持不变... } // 新增/api/auth/firebase-custom-token.js路由 import admin from "firebase-admin"; import { getServerSession } from "next-auth/next"; import { authOptions } from "../auth/[...nextauth]"; export default async function handler(req, res) { const session = await getServerSession(req, res, authOptions); if (!session) { return res.status(401).json({ error: "Unauthorized" }); } try { const customToken = await admin.auth().createCustomToken(session.user.uid); res.status(200).json({ token: customToken }); } catch (error) { res.status(500).json({ error: error.message }); } }
4. 简化Session回调逻辑
移除无效的onAuthStateChanged调用,确保session正确返回用户信息:
// [...nextauth].js中修改callbacks callbacks: { async signIn({ user, account, profile, email, credentials }) { return !!user; }, async jwt({ token, user }) { // 用户登录时存入信息 if (user) { token.user = { uid: user.uid, email: user.email, image: user.image, name: `${user.firstName} ${user.lastName}`, }; } return token; }, async session({ session, token }) { session.user = token.user; return session; }, },
内容的提问来源于stack exchange,提问作者Quan Khuc
相关产品推荐
相关产品推荐

