Next.js SSR环境下Firebase App Check初始化失败求助
问题:Next.js 15 SSR环境中Firebase App Check初始化失败,无法访问Firestore/Storage
我在Next.js 15 + TypeScript项目中集成Firebase,为提升Firestore和Storage的访问安全性,除配置安全规则外,还启用了基于reCAPTCHA v3的Firebase App Check,仅接收已验证请求。客户端侧的App Check初始化验证正常,但尝试在SSR中用客户端获取的App Check Token初始化FirebaseServerApp时,完全无法完成初始化,导致SSR无法访问Firestore或Storage。另外,FirebaseServerAppSettings类型中不存在appCheckToken属性,只能临时用any类型绕过类型检查。
相关代码
/lib/firebase.ts
import { getApps, initializeApp } from "firebase/app"; import { getFirestore } from "firebase/firestore"; import { getStorage } from "firebase/storage"; const firebaseConfig = { apiKey: process.env.NEXT_PUBLIC_FIREBASE_APIKEY, authDomain: "xxxxxxxxxxxxxxxxxxxx.firebaseapp.com", projectId: "xxxxxxxxxxxxxxxxxxxx", storageBucket: "xxxxxxxxxxxxxxxxxxxx.firebasestorage.app", messagingSenderId: "0000000000", appId: "0:00000000000:web:00000000000000000000", measurementId: "G-XXXXXXXXX" }; const app = !getApps().length ? initializeApp(firebaseConfig) : getApps()[0]; const firestore = getFirestore(); const storage = getStorage(); export { firestore, storage, app };
/firebase_initializer.tsx
'use client'; import { app } from '@/app/lib/firebase/firebase'; import { getToken } from 'firebase/app-check'; import { useEffect } from 'react'; const FirebaseInitializer = () => { useEffect(() => { const initializeAppCheckWithRetry = async () => { if (typeof window !== "undefined") { import("firebase/app-check").then(async (firebaseAppCheck) => { const captachp = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY as string; const appcheck = firebaseAppCheck.initializeAppCheck(app, { provider: new firebaseAppCheck.ReCaptchaV3Provider(captachp), isTokenAutoRefreshEnabled: true, }); getToken(appcheck, true).then((token) => { document.cookie = `firebaseAppCheck=${token}`; }); }); } }; initializeAppCheckWithRetry(); }, []); return null; } export default FirebaseInitializer;
blog/page.tsx
import { fetchBlogImageList } from '@/app/lib/actions'; import BlogDetail from '@/app/ui/blog_detail/blog_detail'; import { BlogCategory } from '@/app/utils/enum/blog_category'; import { Blog } from '@/app/utils/type/blog'; import { FirebaseServerAppSettings, initializeServerApp } from 'firebase/app'; import { doc, getDoc, getFirestore, Timestamp } from 'firebase/firestore'; import { cookies } from 'next/headers'; import React from 'react' const Page = async({ params }: { params: Promise<{ id: string }> }) => { const firebaseConfig = { apiKey: process.env.NEXT_PUBLIC_FIREBASE_APIKEY, authDomain: "xxxxxxxxxxxxxxxxxxxx.firebaseapp.com", projectId: "xxxxxxxxxxxxxxxxxxxx", storageBucket: "xxxxxxxxxxxxxxxxxxxx.firebasestorage.app", messagingSenderId: "0000000000", appId: "0:00000000000:web:00000000000000000000", measurementId: "G-XXXXXXXXX" }; const appCheckToken = (await cookies()).get("firebaseAppCheck")?.value; if (!appCheckToken) { throw new Error("App Check token is missing in SSR."); } const firebaseServerAppSettings : any = { appCheckToken : appCheckToken } const serverApp = initializeServerApp(firebaseConfig, firebaseServerAppSettings) const firestore = getFirestore(serverApp); const ref = doc(firestore, "blog", id); const docSnapshot = await getDoc(ref); const data = docSnapshot.data(); const blogData : Blog = { blog_id : data!!.blogId, created_at : (data!!.createdAt as Timestamp).toMillis(), title : data!!.title, category : data!!.category as BlogCategory, thumbnail : data!!.thumbnail, messages : data!!.messages, youtube : data!!.youtube } const imagesList = await fetchBlogImageList(id, blogData.messages.map(message => message.images)); return ( <main className='w-full h-full items-center justify-items-center'> <BlogDetail blog={blogData} blogImages={imagesList}/> </main> ) } export default Page;
解决方案
Firebase Server SDK的initializeServerApp并不支持直接传入appCheckToken参数,正确的做法是通过请求拦截器将Token添加到Firestore/Storage的请求头中,同时优化客户端Token的存储与刷新逻辑。
1. 新建SSR专用Firebase初始化文件
创建/lib/firebase-server.ts,封装带App Check Token的服务端初始化逻辑:
import { getApps, initializeApp } from 'firebase/app'; import { getFirestore, setFirestoreSettings } from 'firebase/firestore'; import { getStorage, ref, getDownloadURL } from 'firebase/storage'; const firebaseConfig = { apiKey: process.env.NEXT_PUBLIC_FIREBASE_APIKEY!, authDomain: "xxxxxxxxxxxxxxxxxxxx.firebaseapp.com", projectId: "xxxxxxxxxxxxxxxxxxxx", storageBucket: "xxxxxxxxxxxxxxxxxxxx.firebasestorage.app", messagingSenderId: "0000000000", appId: "0:00000000000:web:00000000000000000000", measurementId: "G-XXXXXXXXX" }; // 扩展FirestoreSettings类型,支持自定义fetch方法 declare module 'firebase/firestore' { interface FirestoreSettings { fetch?: (url: string, options?: RequestInit) => Promise<Response>; } } export const initializeFirebaseServer = (appCheckToken: string) => { // 避免重复初始化App const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]; // 配置Firestore,添加App Check Token到请求头 const firestore = getFirestore(app); setFirestoreSettings({ fetch: (url, options) => { const headers = new Headers(options?.headers); headers.append('X-Firebase-AppCheck', appCheckToken); return fetch(url, { ...options, headers }); } }); // 配置Storage,添加App Check Token到请求头 const storage = getStorage(app); const getDownloadURLWithAppCheck = async (storageRef: ReturnType<typeof ref>) => { // 替换Storage内部的fetch方法,携带Token (storage as any)._options.fetch = (url: string, options: RequestInit) => { const headers = new Headers(options.headers); headers.append('X-Firebase-AppCheck', appCheckToken); return fetch(url, { ...options, headers }); }; return getDownloadURL(storageRef); }; return { firestore, storage, getDownloadURLWithAppCheck }; };
2. 修改SSR页面的Firebase调用逻辑
更新blog/page.tsx,使用新的服务端初始化方法:
import { fetchBlogImageList } from '@/app/lib/actions'; import BlogDetail from '@/app/ui/blog_detail/blog_detail'; import { BlogCategory } from '@/app/utils/enum/blog_category'; import { Blog } from '@/app/utils/type/blog'; import { doc, getDoc, Timestamp } from 'firebase/firestore'; import { cookies } from 'next/headers'; import { initializeFirebaseServer } from '@/app/lib/firebase-server'; import React from 'react' const Page = async({ params }: { params: { id: string } }) => { // Next.js 15 SSR中params直接是对象,无需Promise解析 const { id } = params; const appCheckToken = (await cookies()).get("firebaseAppCheck")?.value; if (!appCheckToken) { return <div>请完成验证后重试</div>; } const { firestore } = initializeFirebaseServer(appCheckToken); const ref = doc(firestore, "blog", id); const docSnapshot = await getDoc(ref); if (!docSnapshot.exists()) { return <div>该博客不存在</div>; } const data = docSnapshot.data(); const blogData : Blog = { blog_id : data.blogId, created_at : (data.createdAt as Timestamp).toMillis(), title : data.title, category : data.category as BlogCategory, thumbnail : data.thumbnail, messages : data.messages, youtube : data.youtube }; const imagesList = await fetchBlogImageList(id, blogData.messages.map(message => message.images)); return ( <main className='w-full h-full items-center justify-items-center'> <BlogDetail blog={blogData} blogImages={imagesList}/> </main> ) } export default Page;
3. 优化客户端Token的存储与刷新
更新/firebase_initializer.tsx,监听Token变化自动更新Cookie,避免SSR使用过期Token:
'use client'; import { app } from '@/app/lib/firebase/firebase'; import { useEffect } from 'react'; const FirebaseInitializer = () => { useEffect(() => { const initializeAppCheck = async () => { const { initializeAppCheck, ReCaptchaV3Provider, getToken, onTokenChanged } = await import("firebase/app-check"); const captchaKey = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY as string; const appCheckInstance = initializeAppCheck(app, { provider: new ReCaptchaV3Provider(captchaKey), isTokenAutoRefreshEnabled: true, }); // 初始Token写入Cookie const initialToken = await getToken(appCheckInstance, true); document.cookie = `firebaseAppCheck=${initialToken}; path=/; max-age=3600; SameSite=Lax`; // 监听Token刷新,自动更新Cookie onTokenChanged(appCheckInstance, (token) => { if (token) { document.cookie = `firebaseAppCheck=${token}; path=/; max-age=3600; SameSite=Lax`; } }); }; initializeAppCheck(); }, []); return null; } export default FirebaseInitializer;
4. 确保安全规则验证App Check
在Firebase控制台更新Firestore和Storage的安全规则,验证请求携带的App Check Token:
Firestore规则
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if request.app != null; } } }
Storage规则
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { match /{allPaths=**} { allow read, write: if request.app != null; } } }
内容的提问来源于stack exchange,提问作者Tomotomo
相关产品推荐
相关产品推荐

