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

如何通过自定义声明控制页面访问?React+Firebase路由保护问题排查

问题分析与解决方案

一、导航失效的原因及修复

你遇到的导航问题核心在于异步状态获取与路由跳转的时机冲突:

  1. onAuthStateChanged 和获取ID Token自定义声明都是异步操作,组件渲染时状态还未更新,此时返回 <Navigate> 会触发跳转,但后续拿到用户声明后又可能重复触发逻辑,导致路由混乱。
  2. 把 useEffect 里的 navigate 换成 redirect(实际是 navigate(..., { replace: true }))、返回空div,本质是跳过了渲染阶段的跳转逻辑,只在异步状态确定后执行导航,避免了重复触发。

修复代码

调整逻辑顺序,确保只有在用户状态和自定义声明都确定后,再执行路由判断:

import { useEffect, useState } from 'react';
import { Navigate, useNavigate } from 'react-router-dom';
import { getAuth, onAuthStateChanged, getIdTokenResult } from 'firebase/auth';

const ProtectedPage = ({ Component, isSubscribedRoute }) => {
  const [user, setUser] = useState(null);
  const [claims, setClaims] = useState(null);
  const [loading, setLoading] = useState(true);
  const navigate = useNavigate();

  useEffect(() => {
    const auth = getAuth();
    const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
      setLoading(true);
      if (currentUser) {
        const tokenResult = await getIdTokenResult(currentUser);
        const userClaims = tokenResult.claims;
        setUser(currentUser);
        setClaims(userClaims);
        
        // 仅在登录/注册页时跳转learn,避免重复执行
        if (window.location.pathname === '/login' || window.location.pathname === '/register') {
          navigate('/learn', { replace: true });
        }
      } else {
        setUser(null);
        setClaims(null);
        navigate('/login', { replace: true });
      }
      setLoading(false);
    });

    return unsubscribe;
  }, [navigate]);

  // 加载状态显示过渡内容,避免页面闪烁
  if (loading) return <div>Loading...</div>;

  // 权限判断:已登录 + (无需订阅 或 有订阅/试用权限)
  const canAccess = user && (!isSubscribedRoute || (claims?.isSubscribed || claims?.trial));
  
  if (!canAccess) {
    return <Navigate to="/subscribe" replace />;
  }

  return <Component />;
};

export default ProtectedPage;

二、更优实现方案(适配React Router v6+)

作为新手,推荐抛弃HoC模式,改用自定义Hook + 路由守卫组件的方案,更符合现代React函数式编程风格:

1. 自定义Hook:封装认证逻辑

// hooks/useAuth.js
import { useState, useEffect } from 'react';
import { getAuth, onAuthStateChanged, getIdTokenResult } from 'firebase/auth';

export const useAuth = () => {
  const [user, setUser] = useState(null);
  const [claims, setClaims] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const auth = getAuth();
    const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
      setLoading(true);
      if (currentUser) {
        const tokenResult = await getIdTokenResult(currentUser);
        setUser(currentUser);
        setClaims(tokenResult.claims);
      } else {
        setUser(null);
        setClaims(null);
      }
      setLoading(false);
    });

    return unsubscribe;
  }, []);

  return { user, claims, loading };
};

2. 路由守卫组件(替代HoC)

// components/RequireAuth.js
import { Navigate } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';

const RequireAuth = ({ children, requireSubscription = false }) => {
  const { user, claims, loading } = useAuth();

  if (loading) {
    return <div>Loading...</div>; // 可替换为骨架屏组件
  }

  if (!user) {
    return <Navigate to="/login" replace />;
  }

  if (requireSubscription && !(claims?.isSubscribed || claims?.trial)) {
    return <Navigate to="/subscribe" replace />;
  }

  return children;
};

export default RequireAuth;

3. 路由配置(React Router v6.4+ 推荐写法)

// router.js
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import RequireAuth from './components/RequireAuth';
import Login from './pages/Login';
import Register from './pages/Register';
import Learn from './pages/Learn';
import PremiumContent from './pages/PremiumContent';
import Subscribe from './pages/Subscribe';

const router = createBrowserRouter([
  { path: '/login', element: <Login /> },
  { path: '/register', element: <Register /> },
  {
    path: '/learn',
    element: (
      <RequireAuth>
        <Learn />
      </RequireAuth>
    ),
  },
  {
    path: '/premium',
    element: (
      <RequireAuth requireSubscription={true}>
        <PremiumContent />
      </RequireAuth>
    ),
  },
  { path: '/subscribe', element: <Subscribe /> },
]);

export const AppRouter = () => <RouterProvider router={router} />;

方案优势

  • 逻辑解耦:认证逻辑封装在Hook中,可在任何组件复用
  • 路由配置直观:通过组件嵌套声明权限要求,比HoC更易读
  • 符合官方趋势:React Router v6+更推荐用组件/Loader处理路由逻辑,HoC模式已逐渐被淘汰

额外优化建议

  • 全局状态共享:把useAuth的结果放到Context中,避免多个组件重复监听onAuthStateChanged
  • Token刷新:添加定时任务定期刷新ID Token,避免权限判断因Token过期失效
  • 错误处理:在获取ID Token时添加try/catch,处理网络错误或Token失效场景

内容的提问来源于stack exchange,提问作者troy.builds

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 14:45:33