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

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);

问题原因与修复方案

核心问题分析

  1. 客户端Firebase Auth未同步登录状态:CredentialsProvider的authorize方法中使用了客户端Firebase Auth的signInWithEmailAndPassword,该登录状态仅存在于服务器端Node.js环境,不会自动同步到浏览器客户端。
  2. 服务器端误用客户端Firebase Auth实例:getServerSideProps中直接调用客户端auth.currentUser,服务器端是无状态的,每个请求独立,导致状态混乱。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 20:12:11