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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 23:32:32