如何在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状态:
修改后端登录/注册接口,返回自定义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 });前端获取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); } });完善登出逻辑
后端的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); } });修复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
相关产品推荐
相关产品推荐

