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

Flutter中Firebase Auth关联Google与Apple登录的问题排查

Flutter Apple登录与Firebase Auth关联问题排查及修复

问题概述

在Flutter中使用sign_in_with_apple包实现Apple登录,Firebase Auth已设置为**Link accounts that use the same email**。首次用绑定Gmail的AppleID登录时,Firebase控制台显示两个关联登录提供商,但后续操作出现以下异常:

场景1异常

先Apple登录成功(控制台显示Apple提供商),退出后用Google登录,控制台仅显示Google提供商;再次尝试Apple登录失败(Apple仅授权一次),疑问是否需存储AppleID凭证。

场景2异常

先Google登录成功(控制台显示Google提供商),退出后用Apple登录触发[firebase_auth/email-already-in-use]错误。通过fetchSignInMethodsForEmail检测到已有Google登录方式,尝试先调用signInWithGoogle再linkWithCredential,却触发At least one of ID token and access token is required错误。


问题分析

场景1问题根源

  1. 提供商显示问题:Firebase控制台中“当前登录提供商”和“关联的所有提供商”是两个概念。用Google登录后,当前会话的提供商是Google,但Apple提供商仍会关联在用户账号中,需查看用户详情页的“关联提供商”列表确认,而非登录时的临时状态。
  2. Apple登录失败:无需长期存储AppleID凭证,Firebase会自动管理用户会话。失败原因多为:
    • 代码未处理Apple授权取消/失败的情况,导致无效凭证传入Firebase
    • Apple未返回identityToken(如用户已授权过或隐私限制)
    • 退出登录逻辑不完整,导致Firebase会话状态混乱

场景2问题根源

At least one of ID token and access token is required错误核心是Apple凭证无效,代码中存在以下问题:

  1. Apple凭证获取失败时未终止流程,导致空凭证传入后续逻辑
  2. 未校验appleIdCredential.identityToken是否为空
  3. Google退出操作未等待完成就执行登录,导致状态冲突
  4. Google登录未处理用户取消的情况,后续强制解包空值引发异常

代码修复方案

修复Google登录代码

Future<User?> signInWithGoogle() async {
  print('signInWithGoogle() started');
  try {
    final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
    if (googleUser == null) {
      print('Google sign in canceled by user');
      return null;
    }

    final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
    if (googleAuth.idToken == null || googleAuth.accessToken == null) {
      print('Failed to get Google auth tokens');
      return null;
    }

    final AuthCredential credential = GoogleAuthProvider.credential(
      idToken: googleAuth.idToken,
      accessToken: googleAuth.accessToken,
    );

    final UserCredential userCredentials = await _firebaseAuth.signInWithCredential(credential);
    final User? firebaseUser = userCredentials.user;
    print('UserRepository.signInWithGoogle() logged in user is $firebaseUser');

    // 可选:同步Google用户信息到Firebase
    if (firebaseUser != null) {
      final UserInfo googleUserInfo = firebaseUser.providerData.firstWhere(
        (info) => info.providerId == 'google.com',
        orElse: () => throw StateError('Google provider data not found'),
      );
      await firebaseUser.updateDisplayName(googleUserInfo.displayName);
      await firebaseUser.updatePhotoURL(googleUserInfo.photoURL);
      await firebaseUser.reload();
    }

    return firebaseUser;
  } catch (e) {
    print('Google sign in error: $e');
    return null;
  }
}

修复Apple登录代码

Future<User?> signInWithApple() async {
  User? firebaseUser;
  try {
    // 生成nonce防止重放攻击
    final rawNonce = generateNonce();
    final nonce = sha256ofString(rawNonce);

    // 获取Apple凭证
    final AuthorizationCredentialAppleID appleIdCredential = await SignInWithApple.getAppleIDCredential(
      scopes: [
        AppleIDAuthorizationScopes.email,
        AppleIDAuthorizationScopes.fullName,
      ],
      nonce: nonce,
    );

    // 校验凭证有效性
    if (appleIdCredential.identityToken == null) {
      print('Apple identity token is null');
      return null;
    }

    // 创建Firebase Apple凭证
    final OAuthProvider appleProvider = OAuthProvider("apple.com");
    final OAuthCredential oauthCredential = appleProvider.credential(
      idToken: appleIdCredential.identityToken,
      rawNonce: rawNonce,
    );

    final String? displayName = appleIdCredential.givenName != null && appleIdCredential.familyName != null
        ? '${appleIdCredential.givenName} ${appleIdCredential.familyName}'
        : null;
    final String? userEmail = appleIdCredential.email;

    // 处理Apple未返回邮箱的情况(用户已授权过)
    if (userEmail == null) {
      final UserCredential userCredential = await _firebaseAuth.signInWithCredential(oauthCredential);
      return userCredential.user;
    }

    // 检查邮箱对应的登录方式
    final List<String> signInMethods = await _firebaseAuth.fetchSignInMethodsForEmail(userEmail);
    print('UserRepository.signInWithApple() user signInMethods are: $signInMethods');

    if (signInMethods.contains('google.com')) {
      print('UserRepository.signInWithApple() user already has Google credentials');
      // 等待Google退出完成
      await _googleSignIn.signOut();
      // 登录Google账号
      firebaseUser = await signInWithGoogle();
      if (firebaseUser == null) {
        print('Failed to sign in with Google');
        return null;
      }
      // 关联Apple凭证
      final UserCredential linkedCredential = await firebaseUser.linkWithCredential(oauthCredential);
      firebaseUser = linkedCredential.user;
      print('Successfully linked Apple credential to Google user');
    } else {
      // 直接用Apple凭证登录
      final UserCredential userCredential = await _firebaseAuth.signInWithCredential(oauthCredential);
      firebaseUser = userCredential.user;
    }

    // 更新Firebase用户信息
    if (firebaseUser != null) {
      if (displayName != null) await firebaseUser.updateDisplayName(displayName);
      if (userEmail != null) await firebaseUser.updateEmail(userEmail);
      await firebaseUser.reload();
      firebaseUser = await _firebaseAuth.currentUser;
      print('UserRepository.signInWithApple() updated firebaseUser is $firebaseUser');
    }

    return firebaseUser;
  } catch (e) {
    print('UserRepository.signInWithApple() error: $e');
    if (e.toString().contains('firebase_auth/email-already-in-use')) {
      print('Email already in use, verify account linking logic');
    }
    return null;
  }
}

辅助工具函数(Nonce生成)

import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';

String generateNonce([int length = 32]) {
  final charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._';
  final random = Random.secure();
  return List.generate(length, (_) => charset[random.nextInt(charset.length)]).join();
}

String sha256ofString(String input) {
  final bytes = utf8.encode(input);
  final digest = sha256.convert(bytes);
  return digest.toString();
}

完整退出登录逻辑

Future<void> signOut() async {
  await _googleSignIn.signOut();
  await _firebaseAuth.signOut();
}

额外注意事项

  1. 确认Firebase控制台中,Apple和Google登录方式已启用,且Advanced设置中Link accounts that use the same email已勾选
  2. iOS项目需正确配置Apple登录的回调URL,确保凭证能正常返回
  3. Apple首次授权后,后续登录可能静默完成,无需用户重复授权,属于正常行为

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 14:48:11