Angular+Firebase Auth结合SSR时守卫验证前错误渲染登录组件
问题分析与解决方案
这不是Firebase的兼容Bug,而是SSR场景下客户端与服务器端的认证状态不同步导致的问题:
Firebase Auth的用户状态默认存储在客户端的localStorage或会话中,SSR服务器无法直接读取这些客户端数据。当已登录用户刷新/Home页面时,服务器端渲染阶段无法获取用户认证状态,路由守卫会判定用户未登录,先渲染Login页面;等客户端完成hydration(水合)后,才能读取到本地的认证状态,此时守卫再跳转到Home页面,就出现了短暂的Login页面闪烁。
解决思路
核心是让服务器端能获取到用户的认证状态,从而在初始渲染时直接输出Home页面,避免闪烁。具体步骤如下:
1. 客户端:登录后将认证令牌存入Cookie
Firebase的ID Token可以通过Cookie传递到服务器端(服务器能读取Cookie),在用户登录成功后,获取ID Token并写入Cookie:
import { inject } from '@angular/core'; import { Auth, signInWithPopup, GoogleAuthProvider, user } from '@angular/fire/auth'; import { CookieService } from 'ngx-cookie-service'; export class AuthService { private auth = inject(Auth); private cookieService = inject(CookieService); currentUser$ = user(this.auth); async loginWithGoogle() { const provider = new GoogleAuthProvider(); const credential = await signInWithPopup(this.auth, provider); const idToken = await credential.user.getIdToken(); // 将ID Token存入Cookie,服务器端可读取 this.cookieService.set('auth-token', idToken, { path: '/', secure: true, sameSite: 'Strict', maxAge: 3600 * 24 // 有效期1天,与Token有效期匹配 }); } async logout() { await this.auth.signOut(); this.cookieService.delete('auth-token'); } }
2. 服务器端:验证Cookie中的Token,同步用户状态
使用Firebase Admin SDK在服务器端验证ID Token,获取用户信息,并将状态传递给Angular应用:
第一步:安装依赖
npm install firebase-admin @types/firebase-admin
第二步:在服务器入口文件(main.server.ts)初始化Admin并处理认证
import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; import { provideFirebaseApp, initializeApp } from '@angular/fire/app'; import { provideAuth, getAuth } from '@angular/fire/auth'; import { provideServerRendering } from '@angular/platform-server'; import { Request } from 'express'; import * as admin from 'firebase-admin'; import { APP_INITIALIZER, inject, PLATFORM_ID } from '@angular/core'; import { isPlatformServer } from '@angular/common'; import { AuthService } from './app/auth.service'; // 初始化Firebase Admin(确保只初始化一次) if (!admin.apps.length) { admin.initializeApp({ credential: admin.credential.applicationDefault() }); } // 从Request中获取并验证Token async function getServerUser(request: Request) { const token = request.cookies['auth-token']; if (!token) return null; try { return await admin.auth().verifyIdToken(token); } catch (err) { console.error('无效的认证Token:', err); return null; } } bootstrapApplication(AppComponent, { providers: [ provideServerRendering(), provideFirebaseApp(() => initializeApp(environment.firebase)), provideAuth(() => getAuth()), // 提供服务器端用户信息 { provide: 'SERVER_USER', useFactory: () => getServerUser(inject(Request)), deps: [Request] }, // 初始化时同步服务器端用户到AuthService { provide: APP_INITIALIZER, useFactory: () => async () => { if (isPlatformServer(inject(PLATFORM_ID))) { const serverUser = await inject('SERVER_USER'); inject(AuthService).setServerUser(serverUser); } }, multi: true, deps: [PLATFORM_ID, 'SERVER_USER', AuthService] } ] });
3. 调整AuthService,兼容服务器端状态
在AuthService中添加服务器端用户的状态管理,确保客户端水合时状态一致:
import { BehaviorSubject, mergeWith } from 'rxjs'; import { user } from '@angular/fire/auth'; export class AuthService { private auth = inject(Auth); private cookieService = inject(CookieService); // 服务器端用户状态 private serverUser$ = new BehaviorSubject<any>(null); // 合并客户端与服务器端的用户状态 currentUser$ = user(this.auth).pipe(mergeWith(this.serverUser$)); setServerUser(user: any) { this.serverUser$.next(user); } // ... 登录/登出方法 }
4. 更新路由守卫,适配服务器端判断
修改守卫逻辑,在服务器端直接使用验证后的用户状态:
import { inject } from '@angular/core'; import { CanActivateFn, Router } from '@angular/router'; import { AuthService } from './auth.service'; import { isPlatformServer } from '@angular/common'; import { PLATFORM_ID } from '@angular/core'; export const authGuard: CanActivateFn = async (route, state) => { const authService = inject(AuthService); const router = inject(Router); const platformId = inject(PLATFORM_ID); if (isPlatformServer(platformId)) { const serverUser = await inject('SERVER_USER'); return serverUser ? true : router.createUrlTree(['/login']); } else { // 客户端等待AuthState const user = await authService.currentUser$.toPromise(); return user ? true : router.createUrlTree(['/login']); } };
效果验证
完成上述配置后,已登录用户刷新/Home页面时,服务器端能通过Cookie验证用户身份,直接渲染Home页面,不会再出现Login页面的闪烁。
内容的提问来源于stack exchange,提问作者Mac
相关产品推荐
相关产品推荐

