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

Next.js 13+服务端组件加载能否延迟?Firebase鉴权同步问题

问题

我正在开发一个可能转为SaaS的个人项目,当前采用Next.js 13+做前端,结合Firebase身份验证,已完成组件的权限保护。现在遇到的核心问题是:后端用Go编写,仅允许已登录用户访问API接口。我知道可以通过传递UID或令牌到后端SDK验证身份,但服务端组件的异步请求会先于Firebase的onAuthStateChanged更新用户状态,导致初始请求时用户状态为null。

当前流程:

  • layout.tsx加载
  • page.tsx发起API请求
  • loading状态设为true
  • onAuthStateChanged触发(验证用户后显示仪表盘或跳转到登录页)
  • loading状态设为false
  • 展示初始加载的仪表盘

我知道用客户端组件能解决该问题,但想深入探索服务端组件的解决方案。


示例代码

layout.tsx

export default function Layout({children}: {children: React.ReactNode}) {
    const router = useRouter();
    const [loading, setLoading] = useState(true);
    const setUser = useAuthStore((state) => state.setUser);
    
    useEffect(() => {
        const unsubscribe = onAuthStateChanged(auth, (data) => {
            if (data === null) {
                router.push('/');
                return;
            }
            setLoading(false);
            setUser({
                email: data.email
            });
        });

        return () => {
            unsubscribe();
        };
    }, [loading]);

    return (
        <html lang="en">
            <body>
                {loading ? (
                    <Loading/>
                ) : (
                    <> {children} </>
                )}
            </body>
        </html>
    );
}

page.tsx

async function DashBoard() {
    const data = await getExternalAPI(); // 调用fetch函数
    const user = useAuthStore.getState().user; 

    console.log(user);
    return (<div></div>)
}

解决方案

针对Next.js 13+服务端组件的特性,提供几个无需切换到客户端组件的可行思路:

1. 利用Next.js中间件提前拦截验证

Next.js中间件可以在请求到达页面组件前拦截请求,在这里完成Firebase令牌验证,确保只有已登录用户能访问仪表盘页面,同时把用户信息传递给服务端组件。

步骤:

  • 创建middleware.ts文件,从请求cookie中获取Firebase的ID令牌
  • 调用Firebase Admin SDK验证令牌有效性
  • 验证通过后,将用户UID通过请求头传递给页面组件
  • 服务端组件发起API请求时,直接从请求头取用户信息传给Go后端

示例中间件代码:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getAuth } from 'firebase-admin/auth';
import admin from 'firebase-admin';

// 初始化Firebase Admin(确保只初始化一次)
if (!admin.apps.length) {
  admin.initializeApp({
    credential: admin.credential.cert(process.env.FIREBASE_ADMIN_CREDENTIAL as string),
  });
}

export async function middleware(request: NextRequest) {
  // 从cookie获取Firebase ID令牌(需在登录时将令牌存入cookie)
  const idToken = request.cookies.get('firebase-id-token')?.value;

  if (!idToken) {
    // 未登录,重定向到登录页
    return NextResponse.redirect(new URL('/', request.url));
  }

  try {
    // 验证令牌
    const decodedToken = await getAuth().verifyIdToken(idToken);
    // 将用户UID添加到请求头
    const headers = new Headers(request.headers);
    headers.set('x-user-uid', decodedToken.uid);

    return NextResponse.next({
      request: {
        headers,
      },
    });
  } catch (error) {
    // 令牌无效,重定向到登录页
    return NextResponse.redirect(new URL('/', request.url));
  }
}

// 指定中间件作用的路径
export const config = {
  matcher: '/dashboard/:path*',
};

服务端组件中获取用户信息并请求Go后端:

async function DashBoard() {
  // 从请求头获取用户UID
  const headers = await headers();
  const userUid = headers.get('x-user-uid');

  if (!userUid) {
    // 兜底处理,重定向到登录页
    redirect('/');
  }

  // 携带UID请求Go后端
  const data = await fetch(`${process.env.GO_API_URL}/data`, {
    method: 'GET',
    headers: {
      'x-user-uid': userUid,
    },
  });

  const result = await data.json();
  return (<div>{JSON.stringify(result)}</div>)
}

2. 在服务端组件中直接验证令牌

如果不想用中间件,可以在服务端组件中直接从cookie获取Firebase ID令牌,调用Firebase Admin SDK验证,通过后再发起Go后端请求。

示例:

import { cookies } from 'next/headers';
import { getAuth } from 'firebase-admin/auth';
import admin from 'firebase-admin';

// 初始化Firebase Admin
if (!admin.apps.length) {
  admin.initializeApp({
    credential: admin.credential.cert(process.env.FIREBASE_ADMIN_CREDENTIAL as string),
  });
}

async function DashBoard() {
  const cookieStore = cookies();
  const idToken = cookieStore.get('firebase-id-token')?.value;

  if (!idToken) {
    redirect('/');
  }

  try {
    const decodedToken = await getAuth().verifyIdToken(idToken);
    // 验证通过,携带UID请求Go后端
    const data = await fetch(`${process.env.GO_API_URL}/data`, {
      method: 'GET',
      headers: {
        'x-user-uid': decodedToken.uid,
      },
    });

    const result = await data.json();
    return (<div>{JSON.stringify(result)}</div>)
  } catch (error) {
    // 令牌无效,重定向登录页
    redirect('/');
  }
}

3. 优化客户端身份验证流程,延迟服务端组件渲染

调整布局组件逻辑,确保用户身份验证完成且令牌存入cookie后,再渲染服务端组件:

修改layout.tsx:

export default function Layout({children}: {children: React.ReactNode}) {
    const router = useRouter();
    const [loading, setLoading] = useState(true);
    const { user, setUser } = useAuthStore();
    
    useEffect(() => {
        const unsubscribe = onAuthStateChanged(auth, async (data) => {
            if (data === null) {
                router.push('/');
                setLoading(false);
                return;
            }
            // 获取ID令牌并存入cookie
            const idToken = await data.getIdToken();
            document.cookie = `firebase-id-token=${idToken}; path=/; secure; same-site=strict`;
            
            setUser({
                email: data.email,
                uid: data.uid
            });
            setLoading(false);
        });

        return () => {
            unsubscribe();
        };
    }, []);

    // 只有loading为false且user存在时,才渲染服务端组件
    if (loading || !user) {
        return (
            <html lang="en">
                <body>
                    <Loading/>
                </body>
            </html>
        );
    }

    return (
        <html lang="en">
            <body>
                {children}
            </body>
        </html>
    );
}

此时服务端组件会在用户身份验证完成后才开始渲染,可直接从cookie中获取令牌验证,再发起Go后端请求。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 06:55:07