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

Angular 17 DI类拦截器异步操作异常:无限加载/请求拦截求解

Angular Firebase Auth拦截器导致无限加载/请求拦截问题

我在Angular应用中集成了Firebase Auth,为实现授权功能创建了拦截器,直接调用Firebase的idToken异步流获取令牌。我认为YouTube教程里把Token存在localStorage的方案有安全隐患,所以没采用,但现在遇到了浏览器无限加载,或者拦截所有后端HTTP请求(除了Firebase Auth自身请求)的问题。

AuthService代码

import { Injectable, inject, signal } from '@angular/core';
import { Auth, 
         createUserWithEmailAndPassword, 
         sendEmailVerification, 
         sendPasswordResetEmail, 
         signInWithEmailAndPassword, 
         signInWithPopup, 
         signOut, 
         updateProfile,

         user,
         idToken
        } from '@angular/fire/auth';
import { Observable, from, switchMap } from 'rxjs';
import { User } from '../models/user.model';
import { GoogleAuthProvider } from '@angular/fire/auth';


@Injectable({
  providedIn: 'root'
})
export class AuthService {
  firebaseAuth=inject(Auth);
  user$ = user(this.firebaseAuth);
  currentUserSig = signal<User | null | undefined>(undefined);
  authToken$ = idToken(this.firebaseAuth);
  
  register(email:string,username:string,password:string):Observable<void>{
    const promise = createUserWithEmailAndPassword(this.firebaseAuth,email,password).then(
      response => 
        {
          sendEmailVerification(response.user)
          updateProfile(response.user,{displayName:username})
        }
    );
    return from(promise);
  }

  logIn(email:string,password:string):Observable<void>{
     const promise = signInWithEmailAndPassword(
      this.firebaseAuth,
      email,
      password
      ).then(()=>{});
      return from (promise);
  }

  logOut():Observable<void>{
    const promise = signOut(this.firebaseAuth).then(response=> console.log(response));
    return from (promise);
  }

  forgetPassword(email:string):Observable<void>{
     const promise = sendPasswordResetEmail(this.firebaseAuth,email);
     return from (promise);
  }

  googleSignIn():Observable<void>{
        const provider = new GoogleAuthProvider();
        provider.addScope('profile');
        provider.addScope('email');
        provider.setCustomParameters({
          prompt: 'select_account'
        });
   const promise = signInWithPopup(this.firebaseAuth,provider).then(
    response =>{
      console.log(response);
    }
   )
   return from (promise);
  }
}

AuthInterceptor代码

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, switchMap, take } from 'rxjs';
import { AuthService } from '../../services/auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    return this.authService.authToken$.pipe(
      take(1),
      switchMap(token => {
        const authReq = request.clone({
          setHeaders: {
            Authorization: token ? `Bearer ${token}` : ''
          }
        });
        return next.handle(authReq);
      })
    );
  }
}

问题原因与解决方案

核心问题分析

  1. 无限加载/请求循环:Firebase的idToken$流在用户未登录时会持续发出null值,拦截器每次处理请求都会订阅这个流,若请求触发Firebase Auth状态检查,就会形成循环订阅,引发无限加载。
  2. 无差别拦截请求:当前拦截器没有排除无需授权的请求(比如Firebase Auth API、公共接口、静态资源),导致这些请求被添加空的Authorization头,触发后端异常或额外处理,进而阻塞请求。

具体修复步骤

1. 排除无需授权的请求

修改拦截器,跳过Firebase Auth域名、公共接口等无需授权的请求:

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, switchMap, take } from 'rxjs';
import { AuthService } from '../../services/auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  // 定义无需授权的请求域名/路径
  private excludedUrls = [
    'https://identitytoolkit.googleapis.com', // Firebase Auth API
    '/api/public', // 公共接口前缀(根据实际业务调整)
    '/assets' // 静态资源路径
  ];

  constructor(private authService: AuthService) {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    // 检查是否为无需授权的请求,直接放行
    const isExcluded = this.excludedUrls.some(url => request.url.includes(url));
    if (isExcluded) {
      return next.handle(request);
    }

    return this.authService.authToken$.pipe(
      take(1),
      switchMap(token => {
        // 仅当token存在时添加Authorization头,避免空值
        let authReq = request;
        if (token) {
          authReq = request.clone({
            setHeaders: {
              Authorization: `Bearer ${token}`
            }
          });
        }
        return next.handle(authReq);
      })
    );
  }
}

2. 优化Token流的订阅逻辑

在AuthService中创建组合流,仅当用户已登录时才获取Token,避免未登录状态下的无效订阅:

// 在AuthService中添加
import { filter } from 'rxjs';

// ...

// 仅当用户存在时返回Token流
authTokenWithUser$ = this.user$.pipe(
  filter(user => !!user), // 过滤未登录状态
  switchMap(() => this.idToken(this.firebaseAuth))
);

然后修改拦截器使用该流,并添加未登录状态的 fallback:

// 拦截器中替换原有流逻辑
import { catchError } from 'rxjs';

// ...

return this.authService.authTokenWithUser$.pipe(
  take(1),
  switchMap(token => {
    const authReq = request.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
    return next.handle(authReq);
  }),
  // 用户未登录时直接放行请求(根据业务需求调整)
  catchError(() => next.handle(request))
);

3. 移除空Authorization头

原代码中token不存在时会设置Authorization: '',这可能导致后端解析错误,改为仅当token存在时才添加该头,保持原请求的完整性。

额外优化建议

  • 在AuthService中监听user$流更新currentUserSig信号,方便组件快速获取用户状态:
// AuthService构造函数中添加
constructor() {
  this.user$.subscribe(user => {
    this.currentUserSig.set(user ? {
      id: user.uid,
      email: user.email,
      displayName: user.displayName
    } : null);
  });
}

内容的提问来源于stack exchange,提问作者Berlin Johns. M

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 03:35:10