Angular 18 SSR模式下调用Firestore遇权限错误求助
Angular 18 SSR + Firestore 权限错误排查求助
用Angular CLI创建了启用SSR的Angular 18应用,用于SEO优化,搭配Firebase(开发阶段用模拟器)和Firestore存储博客文章。非SSR场景下通过AngularFire的collectionData()拉取数据正常,但SSR模式下遇到权限错误,多次排查无果,特此求助。
核心代码片段
基础服务(TestServiceService)
import { inject, Injectable } from '@angular/core'; import { collection, collectionData, Firestore } from '@angular/fire/firestore'; @Injectable({ providedIn: 'root' }) export class TestServiceService { private firestore = inject(Firestore); constructor() { } getTests(){ return collectionData(collection(this.firestore, 'tests')); } }
AppComponent 逻辑
import { Component, inject, OnInit, signal } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { TestServiceService } from './test-service.service'; import { JsonPipe } from '@angular/common'; @Component({ selector: 'app-root', standalone: true, imports: [RouterOutlet, JsonPipe], templateUrl: './app.component.html', styleUrl: './app.component.scss' }) export class AppComponent implements OnInit { private testService = inject(TestServiceService); tests = signal<any[]>([]); ngOnInit() { this.testService.getTests().subscribe((tests: any) => { this.tests.set(tests); }); } }
客户端配置(app.config.ts)
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; import { provideClientHydration } from '@angular/platform-browser'; import { initializeApp, provideFirebaseApp } from '@angular/fire/app'; import { getAuth, provideAuth } from '@angular/fire/auth'; import { getFirestore, provideFirestore } from '@angular/fire/firestore'; import { getFunctions, provideFunctions } from '@angular/fire/functions'; import { getStorage, provideStorage } from '@angular/fire/storage'; import { environment } from '../environments/environment'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideClientHydration(), provideFirebaseApp(() => initializeApp(environment.firebaseConfig)), provideAuth(() => getAuth()), provideFirestore(() => getFirestore()), provideFunctions(() => getFunctions()), provideStorage(() => getStorage()) ] };
服务端配置(app.config.server.ts)
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; import { provideServerRendering } from '@angular/platform-server'; import { appConfig } from './app.config'; const serverConfig: ApplicationConfig = { providers: [ provideServerRendering(), ] }; export const config = mergeApplicationConfig(appConfig, serverConfig);
路由配置(app.routes.ts)
曾尝试用路由解析加载数据,后注释:
import { Routes } from '@angular/router'; import { AppComponent } from './app.component'; export const routes: Routes = [ { path: '', component: AppComponent, // resolve: { // tests: () => inject(TestServiceService).getTests() // } } ];
Firestore 规则
已设置为开放权限:
service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if request.time < timestamp.date(2024, 10, 22); } } }
遇到的错误信息
执行后首先抛出权限错误:
ERROR FirebaseError: Missing or insufficient permissions.
随后伴随连接超时警告:
[2024-09-23T15:32:16.455Z] @firebase/firestore: Firestore (10.13.2): GrpcConnection RPC 'Listen' stream 0x1b30eee5 error. Code: 1 Message: 1 CANCELLED: Disconnecting idle stream. Timed out waiting for new target
尝试切换为Firebase Admin SDK时,出现Vite模块不可访问错误。
自动生成的服务端入口(server.ts)
import { APP_BASE_HREF } from '@angular/common'; import { CommonEngine } from '@angular/ssr'; import express from 'express'; import { fileURLToPath } from 'node:url'; import { dirname, join, resolve } from 'node:path'; import bootstrap from './src/main.server'; export function app(): express.Express { const server = express(); const serverDistFolder = dirname(fileURLToPath(import.meta.url)); const browserDistFolder = resolve(serverDistFolder, '../browser'); const indexHtml = join(serverDistFolder, 'index.server.html'); const commonEngine = new CommonEngine(); server.set('view engine', 'html'); server.set('views', browserDistFolder); server.get('**', express.static(browserDistFolder, { maxAge: '1y', index: 'index.html', })); server.get('**', (req, res, next) => { const { protocol, originalUrl, baseUrl, headers } = req; commonEngine .render({ bootstrap, documentFilePath: indexHtml, url: `${protocol}://${headers.host}${originalUrl}`, publicPath: browserDistFolder, providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], }) .then((html) => res.send(html)) .catch((err) => next(err)); }); return server; } function run(): void { const port = process.env['PORT'] || 4000; const server = app(); server.listen(port, () => { console.log(`Node Express server listening on http://localhost:${port}`); }); } run();
客户端/服务端入口文件
客户端 main.ts
import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent, appConfig) .catch((err) => console.error(err));
服务端 main.server.ts
import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; import { config } from './app/app.config.server'; const bootstrap = () => bootstrapApplication(AppComponent, config); export default bootstrap;
解决方向
1. 区分环境初始化Firebase
SSR时服务端运行在Node环境,不能复用客户端SDK初始化逻辑:
- 在服务端配置中单独初始化Firebase Admin SDK(禁止在客户端代码中引入Admin SDK,避免Vite打包错误)
- 修改
app.config.server.ts替换Firestore Provider:import { mergeApplicationConfig, ApplicationConfig, Provider } from '@angular/core'; import { provideServerRendering } from '@angular/platform-server'; import { appConfig } from './app.config'; import * as admin from 'firebase-admin'; import { Firestore } from '@angular/fire/firestore'; // 仅在服务端初始化Admin SDK if (!admin.apps.length) { admin.initializeApp({ credential: admin.credential.applicationDefault() }); } // 绑定Admin Firestore实例到Angular依赖注入 const serverFirestoreProvider: Provider = { provide: Firestore, useValue: admin.firestore() }; const serverConfig: ApplicationConfig = { providers: [ provideServerRendering(), serverFirestoreProvider ] }; export const config = mergeApplicationConfig(appConfig, serverConfig);
2. 本地模拟器服务端适配
如果使用Firebase模拟器,需在Admin SDK初始化后指定模拟器地址:
admin.firestore().settings({ host: 'localhost:8080', ssl: false });
3. 确保SSR渲染前数据加载完成
SSR渲染时需保证数据在页面生成前获取完毕,建议用firstValueFrom替代订阅:
- 修改服务方法:
import { firstValueFrom } from 'rxjs'; async getTests() { const data$ = collectionData(collection(this.firestore, 'tests')); return firstValueFrom(data$); } - 启用路由解析器:
resolve: { tests: () => inject(TestServiceService).getTests() }
4. 验证Firestore规则有效性
确认当前时间早于规则中的timestamp.date(2024, 10, 22),测试阶段可临时改为allow read, write: if true;排除规则问题。
5. 配置服务端凭据
本地开发需下载Firebase服务账号密钥,设置环境变量GOOGLE_APPLICATION_CREDENTIALS指向密钥文件路径,确保Admin SDK有权限访问Firestore。
内容的提问来源于stack exchange,提问作者Martin Carre
相关产品推荐
相关产品推荐

