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

如何在React+Express前后端间持久化Firebase Auth状态?

后端使用Firebase Auth,前端无法获取相同Auth实例导致刷新后登出

我在后端使用Firebase Auth实现注册、登录和登出逻辑,但前端无法同步到相同的Auth状态,刷新页面后用户会自动登出,状态无法持久化。我不想把认证逻辑迁移到前端,只希望前后端能共享Auth状态。

后端代码

'use strict';

import { firebaseAdmin, auth } from '../firebase.js';
import deleteCollection from '../helpers/deleteCollection.js';
import User from '../models/user.js';
import {
  createUserWithEmailAndPassword,
  updateProfile,
  signInWithEmailAndPassword,
  signOut,
  setPersistence,
  browserLocalPersistence,
} from 'firebase/auth';

const firestore = firebaseAdmin.firestore();

const register = async (req, res, next) => {
  try {
    // name, email, password
    const { name, email, password, avatar } = req.body;
    console.log('sent from frontend', { name, email, password });
    // Check if email or password were sent
    if (!email || !password) {
      return res.status(422).json({
        email: 'Email is required !',
        password: 'Password is required !',
      });
    }
    const usersCollection = firestore.collection('users');
    // Reference to a QuerySnapshot whith all users that have the requested name
    const userSnapshot = await usersCollection.where('name', '==', name).get();
    // Check if user already exists:
    if (!userSnapshot.empty) {
      throw new Error('Username is taken !');
    } else {
      await setPersistence(auth, browserLocalPersistence);
      // Firebase Auth Create User
      await createUserWithEmailAndPassword(auth, email, password);
      // User is signed in
      const user = auth.currentUser;
      if (user) {
        await updateProfile(user, {
          displayName: name,
        });
        const setUser = {
          id: user.uid,
          name: user.displayName,
          avatar: avatar,
        };
        await usersCollection.doc(setUser.id).set(setUser);
        res.status(201).send(setUser);
      } else {
        throw new Error('No user');
      }
    }
  } catch (error) {
    const errorCode = error.code;
    const errorMessage = error.message;

    res.status(400).send(errorMessage);
    console.log(errorCode, errorMessage);
  }
};

const login = async (req, res, next) => {
  try {
    const { email, password } = req.body;

    await setPersistence(auth, browserLocalPersistence);
    const userCred = await signInWithEmailAndPassword(auth, email, password);

    const usersCollection = firestore.collection('users');
    const userSnapshot = await usersCollection
      .where('name', '==', userCred.user.displayName)
      .get();
    if (userSnapshot.empty) {
      throw new Error('User does not exist !');
    } else {
      let user;

      userSnapshot.forEach((doc) => (user = { ...doc.data() }));
      res.status(200).send(user);
    }
  } catch (error) {
    res.status(404).send(error.message);
    console.log(error);
  }
};

const logout = async (req, res, next) => {
  try {
    // const { name, email, password, avatar } = req.body;

    await signOut(auth);
    res.sendStatus(200);
  } catch (error) {
    const errorCode = error.code;
    const errorMessage = error.message;

    res.status(404).send(errorMessage);
    console.log(error);
  }
};

前端Redux调用代码

API请求层

const register = async (userData) => {
  const response = await axios.post(API_REGISTER, userData, {
    headers: {
      // Overwrite Axios's automatically set Content-Type
      'Content-Type': 'application/json',
    },
  });

  if (response.data) {
    // localStorage.setItem('user', JSON.stringify(response.data));
  }
  return response.data;
};

const login = async (userData) => {
  const response = await axios.post(API_LOGIN, userData, {
    headers: {
      // Overwrite Axios's automatically set Content-Type
      'Content-Type': 'application/json',
    },
  });

  if (response.data) {
    // localStorage.setItem('user', JSON.stringify(response.data));
  }
  return response.data;
};

const logout = async () => {
  const response = await axios.get(`${API_LOGOUT}`);

  if (response.data) {
    localStorage.removeItem('user');
  }
  return response.data;
};

Redux Thunk异步操作

export const register = createAsyncThunk(
  'user/register',
  async (user, thunkAPI) => {
    try {
      return await userService.register(user);
    } catch (error) {
      return thunkAPI.rejectWithValue(error.response.data);
    }
  }
);

export const login = createAsyncThunk('user/login', async (user, thunkAPI) => {
  try {
    return await userService.login(user);
  } catch (error) {
    return thunkAPI.rejectWithValue(error.response.data);
  }
});

export const logout = createAsyncThunk('user/logout', async (_, thunkAPI) => {
  try {
    return await userService.logout();
  } catch (error) {
    return thunkAPI.rejectWithValue(error.response.data);
  }
});

PrivateRoute组件代码

import { useSelector } from 'react-redux';
import { Navigate, useLocation } from 'react-router-dom';

import { auth } from '../../firebase';
import { useAuthState } from 'react-firebase-hooks/auth';
import { useEffect } from 'react';
import { useState } from 'react';

let isAuth;

export default function PrivateRoute({ children }) {
  const location = useLocation();
  const [user, setUser] = useState();
  // const isAuth = useSelector((state) => state.user.user);

  // const [user, loading, error] = useAuthState(auth);

  // useEffect(() => {
  //   if (loading) return;
  //   if (user) {
  //     isAuth = true;
  //     console.log(user);
  //   }
  // }, [user, loading]);

  useEffect(() => {
    auth.onAuthStateChanged(setUser);
  }, []);

  return user ? (
    children
  ) : (
    <Navigate
      replace={true}
      to='/login'
      state={{ from: `${location.pathname}${location.search}` }}
    />
  );
}

问题根源与解决方法

核心问题

后端的Firebase Auth实例(Admin SDK)和前端的Firebase Auth实例是完全独立的:

  • 后端Admin SDK的auth对象运行在服务器端,它的会话状态不会同步到前端浏览器。
  • 你在后端调用setPersistence(auth, browserLocalPersistence)完全无效,因为browserLocalPersistence是前端浏览器环境的持久化策略,服务器端不支持。
  • 前端的auth实例无法感知后端的登录操作,所以刷新页面后没有本地认证状态,导致被PrivateRoute拦截。

解决步骤

要实现前后端Auth状态同步,不需要把所有逻辑迁到前端,只需要调整后端返回认证凭证,前端用凭证初始化本地Auth状态:

  1. 修改后端登录/注册接口,返回自定义Token
    在后端的register和login函数中,生成Firebase自定义Token并返回给前端:

    // register函数中,创建用户后添加:
    const customToken = await auth.createCustomToken(user.uid);
    // 修改返回内容,把token和用户信息一起返回
    res.status(201).send({ ...setUser, token: customToken });
    
    // login函数中,登录成功后添加:
    const customToken = await auth.createCustomToken(userCred.user.uid);
    res.status(200).send({ ...user, token: customToken });
    
  2. 前端获取Token后,用它登录本地Firebase Auth
    在Redux的register和login异步操作中,拿到返回的token后,调用前端Firebase Auth的signInWithCustomToken:

    // 先在userService的register和login中返回带token的数据
    // 然后修改Thunk:
    import { signInWithCustomToken } from 'firebase/auth';
    import { auth } from '../../firebase';
    
    export const register = createAsyncThunk(
      'user/register',
      async (user, thunkAPI) => {
        try {
          const response = await userService.register(user);
          // 用自定义Token登录前端Auth
          await signInWithCustomToken(auth, response.token);
          return response;
        } catch (error) {
          return thunkAPI.rejectWithValue(error.response.data);
        }
      }
    );
    
    export const login = createAsyncThunk('user/login', async (user, thunkAPI) => {
      try {
        const response = await userService.login(user);
        await signInWithCustomToken(auth, response.token);
        return response;
      } catch (error) {
        return thunkAPI.rejectWithValue(error.response.data);
      }
    });
    
  3. 完善登出逻辑
    后端的signOut只处理服务器端的状态(如果有的话),前端需要同时调用本地Firebase Auth的signOut:

    // 修改logout的Thunk:
    import { signOut as firebaseSignOut } from 'firebase/auth';
    
    export const logout = createAsyncThunk('user/logout', async (_, thunkAPI) => {
      try {
        await userService.logout();
        // 前端也登出
        await firebaseSignOut(auth);
        return response.data;
      } catch (error) {
        return thunkAPI.rejectWithValue(error.response.data);
      }
    });
    
  4. 修复PrivateRoute组件
    直接使用useAuthState钩子即可,它会自动处理持久化状态:

    import { useSelector } from 'react-redux';
    import { Navigate, useLocation } from 'react-router-dom';
    
    import { auth } from '../../firebase';
    import { useAuthState } from 'react-firebase-hooks/auth';
    
    export default function PrivateRoute({ children }) {
      const location = useLocation();
      const [user, loading] = useAuthState(auth);
    
      // 加载状态可以加个加载组件,避免瞬间跳转
      if (loading) {
        return <div>Loading...</div>; // 替换成你的加载UI
      }
    
      return user ? (
        children
      ) : (
        <Navigate
          replace={true}
          to='/login'
          state={{ from: `${location.pathname}${location.search}` }}
        />
      );
    }
    

为什么这样可行

  • 自定义Token是后端Admin SDK生成的,前端用它登录后,Firebase Auth会自动把认证状态持久化到浏览器本地(默认就是localStorage),刷新页面时会自动恢复。
  • 前后端通过自定义Token完成状态同步,后端依然保留用户创建、用户名校验等核心逻辑,不需要迁移到前端。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 01:55:21