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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 00:24:53