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
相关产品推荐
相关产品推荐

