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

Firebase Auth手机认证遇auth/quota-exceeded配额超限错误求助

Firebase免费版手机认证触发配额超限问题

我正在测试一款使用Firebase免费版手机认证功能的应用,已按照官方文档将测试用手机号注册为虚拟号码,但测试时出现以下错误:

FirebaseError: Firebase: Exceeded quota. (auth/quota-exceeded).
    at _errorWithCustomMessage (index-e3d5d3f4.js:476:20)
    at index-e3d5d3f4.js:908:23
    at Generator.next (<anonymous>)
    at asyncGeneratorStep (asyncToGenerator.js:3:1)
    at _next (asyncToGenerator.js:22:1)
    at _ZoneDelegate.invoke (zone.js:368:26)
    at Zone.run (zone.js:129:43)
    at zone.js:1257:36
    at _ZoneDelegate.invokeTask (zone.js:402:31)
    at Zone.runTask (zone.js:173:47)
node_modules_ionic_core_dist_esm_ion-backdrop_entry_js.js:2 ERROR Error: Uncaught (in promise): FirebaseError: Firebase: Exceeded quota. (auth/quota-exceeded).
FirebaseError: Firebase: Exceeded quota. (auth/quota-exceeded).
    at _errorWithCustomMessage (index-e3d5d3f4.js:476:20)
    at index-e3d5d3f4.js:908:23
    at Generator.next (<anonymous>)
    at asyncGeneratorStep (asyncToGenerator.js:3:1)
    at _next (asyncToGenerator.js:22:1)
    at _ZoneDelegate.invoke (zone.js:368:26)
    at Zone.run (zone.js:129:43)
    at zone.js:1257:36
    at _ZoneDelegate.invokeTask (zone.js:402:31)
    at Zone.runTask (zone.js:173:47)
    at resolvePromise (zone.js:1193:31)
    at resolvePromise (zone.js:1147:17)
    at zone.js:1260:17
    at _ZoneDelegate.invokeTask (zone.js:402:31)
    at core.mjs:10735:55
    at AsyncStackTaggingZoneSpec.onInvokeTask (core.mjs:10735:36)
    at _ZoneDelegate.invokeTask (zone.js:401:60)
    at Object.onInvokeTask (core.mjs:11048:33)
    at _ZoneDelegate.invokeTask (zone.js:401:60)
    at Zone.runTask (zone.js:173:47)

我不清楚为何会触发配额超限,因为查看配额使用情况并未显示任何限制已被触发。此外,我该如何准确知晓具体超出了哪项限制,以及超限的确切原因?


补充代码:

TS组件代码:

import { Component, OnInit } from '@angular/core';
import { LoadingController } from '@ionic/angular';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { MaskitoOptions, MaskitoElementPredicateAsync } from '@maskito/core';
import { AuthService } from 'src/app/services/auth.service';

@Component({
  selector: 'app-phone-method',
  templateUrl: './phone-method.component.html',
  styleUrls: ['./phone-method.component.scss'],
})
export class PhoneMethodComponent  implements OnInit {
  phoneForm: FormGroup;
  phoneNumber: string;
  constructor(private formBuilder: FormBuilder, private authService: AuthService, private router: Router, private loadingCtrl: LoadingController) {
    this.phoneForm = this.formBuilder.group({
      phoneNum: ['', Validators.compose([Validators.required, Validators.pattern(/^\+\d{2}\s\(\d{3}\)\s\d{3}-\d{4}$/)])
    ]});
    this.phoneNumber="";
  }

  async sendSMS() {
    this.phoneNumber=this.phoneForm.get('phoneNum')?.value;
    const loading = await this.loadingCtrl.create({
      message: 'Enviando mensaje...',
      cssClass: 'custom-loading'
    });
    this.authService.signInWithPhoneNumber(this.phoneNumber,loading).then(result=>{
      loading.dismiss();
      let encodedPhoneNumber = encodeURIComponent(this.phoneNumber);
      this.router.navigate(['/phone-otp',encodedPhoneNumber]);
    });

    /*let phone=this.phoneForm.get('phoneNum')?.value;
    if (this.phoneForm.valid) {
      console.log(phone);
    }*/
  }

  goBack() {
    // Navigate one step back in the browser's history
    this.router.navigate(['/'])
  }

  readonly phoneMask: MaskitoOptions = {
    mask: ['+','5','7',' ','(',/\d/,/\d/,/\d/,')',' ',/\d/,/\d/,/\d/,'-',/\d/,/\d/,/\d/,/\d/],
  };

  readonly maskPredicate: MaskitoElementPredicateAsync = async (el) => (el as HTMLIonInputElement).getInputElement();

  ngOnInit() {}
}

HTML模板代码:

<ion-content class="ion-padding">
  <a (click)="goBack()">&lt; Atrás</a>
  <h3 class="tittle">Ingresa tu número celular</h3>
  <p class="description">Te enviaremos un código para confirmarlo</p>
  <form [formGroup]="phoneForm" (ngSubmit)="sendSMS()">
    <ion-item>
      <ion-input class="margin-top"
        label="Número de celular"
        type="tel"
        class="custom"
        formControlName="phoneNum"
        labelPlacement="floating"
        placeholder="+57 (xxx) xxx-xxxx"
        [maskito]="phoneMask"
        [maskitoElement]="maskPredicate">
      </ion-input>
    </ion-item>
    <div id="recaptcha-container"></div>
  <div class="ion-text-center margin-top">
    <ion-button type="submit" *ngIf="phoneForm.valid" fill="outline">Enviar SMS</ion-button>
  </div>
</form>
</ion-content>

Auth服务代码:

import { Injectable } from '@angular/core';
import {Auth,signInWithEmailAndPassword,createUserWithEmailAndPassword,signInWithPhoneNumber,RecaptchaVerifier,signOut} from '@angular/fire/auth';

@Injectable({
    providedIn: 'root'
})
export class AuthService {

  appVerifier: any;
  confirmationResult: any;

  constructor(private _fireAuth: Auth) {
    _fireAuth.languageCode ='es';
    _fireAuth.settings.appVerificationDisabledForTesting=true;
  }

  recaptcha(loading: HTMLIonLoadingElement) {
    this.appVerifier = new RecaptchaVerifier('recaptcha-container', {
      size: 'invisible',
      callback: (response: any) => {
        loading.present();
      },
      'expired-callback': () => {
      }
    }, this._fireAuth);
  }

  async signInWithPhoneNumber(phoneNumber: string, loading: HTMLIonLoadingElement) {
    try {
      if(!this.appVerifier){
        this.recaptcha(loading);
      }
      const confirmationResult = await signInWithPhoneNumber(this._fireAuth, phoneNumber, this.appVerifier);
      this.confirmationResult = confirmationResult;
      loading.dismiss();
      return confirmationResult;
    } catch(e) {
      console.log(e);
      throw(e);
    }
  }

  async verifyOtp(otp: any, loading: HTMLIonLoadingElement) {
    try {
      if(!this.appVerifier){
        this.recaptcha(loading);
      }
      const result = await this.confirmationResult.confirm(otp);
      const user = result?.user;
      loading.dismiss();
      return user;
    } catch(e) {
      console.log(e);
    }
  }
  /*
    async register({ email, password }) {
        try {
            const user = await createUserWithEmailAndPassword(this.auth, email, password);
            return user;
        } catch (e) {
            return null;
        }
    }

    async login({ email, password }) {
        try {
            const user = await signInWithEmailAndPassword(this.auth, email, password);
            return user;
        } catch (e) {
            return null;
        }
    }*/

    logout() {
        return signOut(this._fireAuth);
    }
}

问题解答

1. 配额超限但控制台未显示的原因

Firebase免费版的手机认证有几个易被忽略的限制:

  • 单手机号请求频率限制:免费版对单个测试手机号的请求频率有严格限制(比如短时间内多次发送验证码),这类限制不会在配额控制台的总用量中显示,只会触发auth/quota-exceeded错误。
  • Recaptcha验证请求配额:即使启用了appVerificationDisabledForTesting,Recaptcha Verifier的请求仍会占用Google Cloud的Recaptcha配额,而这个配额不会在Firebase控制台单独展示。
  • 统计延迟:Firebase配额用量统计存在延迟,刚触发限制时控制台可能还未更新数据。

2. 如何定位具体超限项

  • 查看Cloud Logging日志:进入项目的Cloud Logging,筛选auth/quota-exceeded相关日志,日志中会包含具体的配额名称(比如PHONE_AUTH_REQUESTS_PER_MINUTE_PER_NUMBER),明确哪项限制被触发。
  • 检查测试流程:如果短时间内重复点击发送验证码按钮,大概率是单手机号的频率限制。你的代码未对发送按钮做禁用处理,容易导致重复请求触发限制。
  • 确认测试号码状态:确保测试号码已正确添加到Firebase项目的测试号码列表中,未添加的号码会占用正式配额(免费版额度极低)。

3. 解决建议

  • 添加按钮禁用逻辑:在sendSMS方法中,发送请求前禁用按钮,请求完成后再启用,避免重复提交:
    async sendSMS() {
      const submitBtn = document.querySelector('ion-button[type="submit"]');
      if(submitBtn) submitBtn.setAttribute('disabled', 'true');
      
      this.phoneNumber=this.phoneForm.get('phoneNum')?.value;
      const loading = await this.loadingCtrl.create({
        message: 'Enviando mensaje...',
        cssClass: 'custom-loading'
      });
      try {
        await this.authService.signInWithPhoneNumber(this.phoneNumber,loading);
        loading.dismiss();
        let encodedPhoneNumber = encodeURIComponent(this.phoneNumber);
        this.router.navigate(['/phone-otp',encodedPhoneNumber]);
      } catch(e) {
        loading.dismiss();
        console.error(e);
      } finally {
        if(submitBtn) submitBtn.removeAttribute('disabled');
      }
    }
    
  • 调整Recaptcha配置:既然启用了appVerificationDisabledForTesting,可在测试环境中跳过Recaptcha验证,避免占用配额。
  • 查看Cloud Console配额详情:进入Google Cloud Console的IAM与管理>配额页面,筛选Firebase Authentication和reCAPTCHA相关配额,查看具体用量和限额。

内容的提问来源于stack exchange,提问作者AlejoDev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 07:12:04