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问题根源
- 提供商显示问题:Firebase控制台中“当前登录提供商”和“关联的所有提供商”是两个概念。用Google登录后,当前会话的提供商是Google,但Apple提供商仍会关联在用户账号中,需查看用户详情页的“关联提供商”列表确认,而非登录时的临时状态。
- Apple登录失败:无需长期存储AppleID凭证,Firebase会自动管理用户会话。失败原因多为:
- 代码未处理Apple授权取消/失败的情况,导致无效凭证传入Firebase
- Apple未返回
identityToken(如用户已授权过或隐私限制) - 退出登录逻辑不完整,导致Firebase会话状态混乱
场景2问题根源
At least one of ID token and access token is required错误核心是Apple凭证无效,代码中存在以下问题:
- Apple凭证获取失败时未终止流程,导致空凭证传入后续逻辑
- 未校验
appleIdCredential.identityToken是否为空 - Google退出操作未等待完成就执行登录,导致状态冲突
- 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(); }
额外注意事项
- 确认Firebase控制台中,Apple和Google登录方式已启用,且Advanced设置中
Link accounts that use the same email已勾选 - iOS项目需正确配置Apple登录的回调URL,确保凭证能正常返回
- Apple首次授权后,后续登录可能静默完成,无需用户重复授权,属于正常行为
内容的提问来源于stack exchange,提问作者Vincenzo
相关产品推荐
相关产品推荐

