Firebase直链导航时AuthGuard未正确跳转未认证用户至首页
自定义AuthGuard权限控制问题与修复
问题描述
我在使用自定义AuthGuard实现权限控制,需求是当用户不具备访问页面所需权限时自动跳转至首页,但当前实现存在以下问题:
- 若启用注释掉的
router.push('/home')代码,无论是已认证还是未认证用户,通过直链导航页面时都会被跳转至首页; - 注释该代码后,已认证用户可正常直链访问受保护页面,但未认证用户无法跳转至首页,页面持续加载。
现有代码
AuthGuard组件
import { ReactNode, useEffect, useState } from 'react'; import { useRouter } from 'next/router'; import { useFirebaseAuth } from '@/contexts/FirebaseAuthContext'; import { can } from '@/lib/acl'; import { Loader } from '@/components/Loader'; export type ChildrenType = { children: ReactNode; requiredPermission?: { action: string; subject: string }; }; export default function AuthGuard({ children, requiredPermission }: ChildrenType) { const { user, role, loading } = useFirebaseAuth(); const router = useRouter(); const [isAuthorized, setIsAuthorized] = useState(false); useEffect(() => { console.log('Loading', loading); console.log('AuthGuard user', user); if (!loading) { if (user && role && requiredPermission) { if (!can(role, requiredPermission.action, requiredPermission.subject)) { router.push('/dashboard'); setIsAuthorized(false); } else { setIsAuthorized(true); } } // else { // // Redirect to home if he's not logged in (no user) // router.push('/home'); // } } }, [role, loading, requiredPermission, router, user]); if (loading || !isAuthorized) { return <Loader />; } return <> {children} </>; }
useFirebaseAuth Hook
import React, { createContext, useContext, useEffect, useState } from 'react'; import { onAuthStateChanged, sendPasswordResetEmail, signOut, User } from 'firebase/auth'; import { auth } from '@/config/firebase/utils'; import { Role } from '@/store/types'; import { useRouter } from 'next/router'; import { clearUserSubscriptionData } from '@/store/slices/userSubscriptionSlice'; import { useDispatch } from 'react-redux'; import { AppDispatch } from '@/store'; import { useUserSubscription } from '@/hooks/useUserSubscription'; interface AuthContextType { user: User | null; role: Role; loading: boolean; logout: () => Promise<void>; sendForgotPasswordEmail: (email: string) => Promise<void>; } const FirebaseAuthContext = createContext<AuthContextType | null>(null); export const FirebaseAuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [user, setUser] = useState<User | null>(null); const [role, setRole] = useState<Role>(Role.VISITOR); const dispatch = useDispatch<AppDispatch>(); const router = useRouter(); const [loading, setLoading] = useState(false); const { fetchUserSubscription } = useUserSubscription(); useEffect(() => { const authStateChange = onAuthStateChanged(auth, async (user) => { setLoading(true); if (user) { setUser(user); const token = await user.getIdTokenResult(); await fetchUserSubscription(); const role = token.claims.role ? (token.claims.role as Role) : Role.GUEST; setRole(role); } else { setUser(null); setRole(Role.VISITOR); dispatch(clearUserSubscriptionData()); } setLoading(false); }); return () => authStateChange(); }, [auth, user, dispatch]); const sendForgotPasswordEmail = async (email: string) => { try { await sendPasswordResetEmail(auth, email); } catch (error) { throw new Error('Unable to send password reset email. Please try again.'); } }; const logout = async () => { setLoading(true); try { await signOut(auth); setUser(null); setRole(Role.VISITOR); dispatch(clearUserSubscriptionData()); router.push('/home'); } catch (error) { console.error('Logout Error:', error); } finally { setLoading(false); } }; return ( <FirebaseAuthContext.Provider value={{ user, role, loading, logout, sendForgotPasswordEmail }} > {children} </FirebaseAuthContext.Provider> ); }; export const useFirebaseAuth = () => { const context = useContext(FirebaseAuthContext); if (!context) { throw new Error( 'useFirebaseAuth must be used within a FirebaseAuthProvider' ); } return context; };
控制台日志
已认证用户访问受保护页面(正常)
Loading false AuthGuard.tsx:22:12 AuthGuard user null AuthGuard.tsx:23:12 Loading false AuthGuard.tsx:22:12 AuthGuard user null AuthGuard.tsx:23:12 Loading true AuthGuard.tsx:22:12 AuthGuard user Object { … } AuthGuard.tsx:23:12 Loading true AuthGuard.tsx:22:12 AuthGuard user Object { … } AuthGuard.tsx:23:12 Loading false AuthGuard.tsx:22:12 AuthGuard user Object { … } AuthGuard.tsx:23:12 Loading false AuthGuard.tsx:22:12 AuthGuard user Object { … }
未认证用户访问受保护页面(异常,持续加载)
Loading false AuthGuard.tsx:22:12 AuthGuard user null AuthGuard.tsx:23:12 Loading false AuthGuard.tsx:22:12 AuthGuard user null AuthGuard.tsx:23:12 Loading false AuthGuard.tsx:22:12 AuthGuard user null
问题分析
- 启用跳转代码时的问题:useEffect依赖数组包含
user,组件初始化时user为null,会立即触发跳转逻辑。而Firebase的auth状态初始化是异步的,已认证用户的user会从null变为有效值,但此时跳转已经执行,导致用户被强制跳走。 - 未启用跳转代码时的问题:未认证用户的
isAuthorized始终为false,且loading已经变为false,组件一直返回<Loader />,没有触发跳转,也没有更新状态终止加载。 - 额外问题:
useFirebaseAuth中的useEffect依赖包含user,会导致循环触发(因为onAuthStateChanged回调中会调用setUser,进而触发useEffect重新执行)。
修复方案
1. 修正useFirebaseAuth的useEffect依赖
移除依赖中的user,避免循环触发:
useEffect(() => { const authStateChange = onAuthStateChanged(auth, async (user) => { setLoading(true); if (user) { setUser(user); const token = await user.getIdTokenResult(); await fetchUserSubscription(); const role = token.claims.role ? (token.claims.role as Role) : Role.GUEST; setRole(role); } else { setUser(null); setRole(Role.VISITOR); dispatch(clearUserSubscriptionData()); } setLoading(false); }); return () => authStateChange(); }, [auth, dispatch]); // 移除user依赖
2. 修正AuthGuard的逻辑
- 仅在auth状态加载完成后处理权限和跳转
- 未认证用户直接跳转并终止加载状态
- 避免重复跳转(检查当前路由)
修复后的AuthGuard代码:
import { ReactNode, useEffect, useState } from 'react'; import { useRouter } from 'next/router'; import { useFirebaseAuth } from '@/contexts/FirebaseAuthContext'; import { can } from '@/lib/acl'; import { Loader } from '@/components/Loader'; export type ChildrenType = { children: ReactNode; requiredPermission?: { action: string; subject: string }; }; export default function AuthGuard({ children, requiredPermission }: ChildrenType) { const { user, role, loading } = useFirebaseAuth(); const router = useRouter(); const [isAuthorized, setIsAuthorized] = useState(false); const [hasRedirected, setHasRedirected] = useState(false); // 标记是否已跳转 useEffect(() => { if (loading || hasRedirected) return; // 加载中或已跳转则不处理 // 未登录用户直接跳转首页 if (!user) { if (router.pathname !== '/home') { router.push('/home'); setHasRedirected(true); } setIsAuthorized(false); return; } // 已登录但无权限 if (role && requiredPermission) { const hasPermission = can(role, requiredPermission.action, requiredPermission.subject); if (!hasPermission) { if (router.pathname !== '/dashboard') { router.push('/dashboard'); setHasRedirected(true); } setIsAuthorized(false); } else { setIsAuthorized(true); } } else { // 无权限要求时直接授权 setIsAuthorized(true); } }, [role, loading, requiredPermission, router, user, hasRedirected]); if (loading || !isAuthorized) { return <Loader />; } return <> {children} </>; }
修复说明
- 给AuthGuard添加
hasRedirected状态,避免重复触发跳转 - 处理逻辑拆分:先判断未登录情况,再处理已登录的权限校验
- 跳转前检查当前路由,避免不必要的路由跳转
- 修复useFirebaseAuth的循环触发问题,确保auth状态只初始化一次
内容的提问来源于stack exchange,提问作者user2426691
相关产品推荐
相关产品推荐

