在Android Firebase中,首先使用Facebook进行身份验证,然后再使用Google会导致错误。

61
根据 Firebase 文档,如果用户使用凭据进行身份验证,则如果该凭据尚未与其他凭据关联,他应严格使用相同的凭据登录。
换句话说,如果我使用 Google 登录创建帐户,然后(注销后)尝试使用与 Google 凭据相同的电子邮件登录 Facebook 凭据,则应在 logcat 中看到此异常:
"An account already exists with the same email address but different sign-in credentials. Sign in using a provider associated with this email address."
毫不奇怪,我收到了这个异常。但是,如果我使用 Facebook 创建帐户,然后尝试使用 Google 凭据登录,那么此帐户的提供程序(Facebook)将转换为 Google。这次身份验证不会失败,但这不是预期的结果。我想以某种方式将每个用户与特定的凭据关联起来。我该如何解决?您可以查看下面的代码:
public class SignInActivity extends AppCompatActivity implements GoogleApiClient.OnConnectionFailedListener,
        View.OnClickListener {

    private static final String TAG = "SignInActivity";
    private static final int RC_SIGN_IN = 9001;

    private GoogleApiClient mGoogleApiClient;
    private FirebaseAuth mFirebaseAuth;
    private FirebaseAuth.AuthStateListener mFirebaseAuthListener;

    private CallbackManager mCallbackManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_sign_in);

        // Facebook Login
        FacebookSdk.sdkInitialize(getApplicationContext());
        mCallbackManager = CallbackManager.Factory.create();

        LoginButton mFacebookSignInButton = (LoginButton) findViewById(R.id.facebook_login_button);
        mFacebookSignInButton.setReadPermissions("email", "public_profile");

        mFacebookSignInButton.registerCallback(mCallbackManager, new FacebookCallback<LoginResult>() {
            @Override
            public void onSuccess(LoginResult loginResult) {
                Log.d(TAG, "facebook:onSuccess:" + loginResult);
                firebaseAuthWithFacebook(loginResult.getAccessToken());
            }

            @Override
            public void onCancel() {
                Log.d(TAG, "facebook:onCancel");
            }

            @Override
            public void onError(FacebookException error) {
                Log.d(TAG, "facebook:onError", error);
            }
        });

        // Google Sign-In
        // Assign fields
        SignInButton mGoogleSignInButton = (SignInButton) findViewById(R.id.google_sign_in_button);

        // Set click listeners
        mGoogleSignInButton.setOnClickListener(this);

        GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestIdToken(getString(R.string.default_web_client_id))
                .requestEmail()
                .build();
        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .enableAutoManage(this /* FragmentActivity */, this /* OnConnectionFailedListener */)
                .addApi(Auth.GOOGLE_SIGN_IN_API, gso)
                .build();

        // Initialize FirebaseAuth
        mFirebaseAuth = FirebaseAuth.getInstance();

        mFirebaseAuthListener = new FirebaseAuth.AuthStateListener() {
            @Override
            public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) {
                FirebaseUser user = firebaseAuth.getCurrentUser();
                if (user != null) {
                    // User is signed in
                    Log.d(TAG, "onAuthStateChanged:signed_in:" + user.getUid());
                } else {
                    // User is signed out
                    Log.d(TAG, "onAuthStateChanged:signed_out");
                }
            }
        };
    }

    @Override
    public void onStart() {
        super.onStart();
        mFirebaseAuth.addAuthStateListener(mFirebaseAuthListener);
    }

    @Override
    public void onStop() {
        super.onStop();
        if (mFirebaseAuthListener != null) {
            mFirebaseAuth.removeAuthStateListener(mFirebaseAuthListener);
        }
    }

    private void firebaseAuthWithGoogle(GoogleSignInAccount acct) {
        Log.d(TAG, "firebaseAuthWithGooogle:" + acct.getId());
        AuthCredential credential = GoogleAuthProvider.getCredential(acct.getIdToken(), null);
        mFirebaseAuth.signInWithCredential(credential)
                .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
                    @Override
                    public void onComplete(@NonNull Task<AuthResult> task) {
                        Log.d(TAG, "signInWithCredential:onComplete:" + task.isSuccessful());

                        // If sign in fails, display a message to the user. If sign in succeeds
                        // the auth state listener will be notified and logic to handle the
                        // signed in user can be handled in the listener.
                        if (!task.isSuccessful()) {
                            Log.w(TAG, "signInWithCredential", task.getException());
                            Toast.makeText(SignInActivity.this, "Authentication failed.",
                                    Toast.LENGTH_SHORT).show();
                        } else {
                            startActivity(new Intent(SignInActivity.this, MainActivity.class));
                            finish();
                        }
                    }
                });
    }

    private void firebaseAuthWithFacebook(AccessToken token) {
        Log.d(TAG, "handleFacebookAccessToken:" + token);

        final AuthCredential credential = FacebookAuthProvider.getCredential(token.getToken());
        mFirebaseAuth.signInWithCredential(credential)
                .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
                    @Override
                    public void onComplete(@NonNull Task<AuthResult> task) {
                        Log.d(TAG, "signInWithCredential:onComplete:" + task.isSuccessful());

                        // If sign in fails, display a message to the user. If sign in succeeds
                        // the auth state listener will be notified and logic to handle the
                        // signed in user can be handled in the listener.
                        if (!task.isSuccessful()) {
                            Log.w(TAG, "signInWithCredential", task.getException());
                            Toast.makeText(SignInActivity.this, "Authentication failed.",
                                    Toast.LENGTH_SHORT).show();
                        }

                        else {
                            startActivity(new Intent(SignInActivity.this, MainActivity.class));
                            finish();
                        }
                    }
                });
    }

    /*
    private void handleFirebaseAuthResult(AuthResult authResult) {
        if (authResult != null) {
            // Welcome the user
            FirebaseUser user = authResult.getUser();
            Toast.makeText(this, "Welcome " + user.getEmail(), Toast.LENGTH_SHORT).show();

            // Go back to the main activity
            startActivity(new Intent(this, MainActivity.class));
        }
    }
    */

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.google_sign_in_button:
                signIn();
                break;
            default:
                return;
        }
    }

    private void signIn() {
        Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient);
        startActivityForResult(signInIntent, RC_SIGN_IN);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        mCallbackManager.onActivityResult(requestCode, resultCode, data);

        // Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...);
        if (requestCode == RC_SIGN_IN) {
            GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
            if (result.isSuccess()) {
                // Google Sign In was successful, authenticate with Firebase
                GoogleSignInAccount account = result.getSignInAccount();
                firebaseAuthWithGoogle(account);
            } else {
                // Google Sign In failed
                Log.e(TAG, "Google Sign In failed.");
            }
        }
    }

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
        // An unresolvable error has occurred and Google APIs (including Sign-In) will not
        // be available.
        Log.d(TAG, "onConnectionFailed:" + connectionResult);
        Toast.makeText(this, "Google Play Services error.", Toast.LENGTH_SHORT).show();
    }
}

1
是的,我已经检查过了,但我认为可能有一种解决方法。例如,如果您允许一个电子邮件地址拥有多个帐户,那么您就不会再遇到这个问题,但是用户在数据库中将无法关联。 - r3dm4n
9个回答

39

前往身份验证 > 登录提供者,单击电子邮件地址的多个帐户并允许创建具有相同电子邮件地址的多个帐户。

账户电子邮件地址设置


7
请注意:这将为单个用户创建多个UID。如果您正在将Firebase UID与本地数据库绑定,则这不是正确的解决方案。 - Muhammad Saqib
2
这不是OP或任何人想要的,如果有什么的话,这只是一个临时解决方案。 - Hiro

18

51
这真的很烦人。谷歌自动将账户静默转换为谷歌账户,但对于Facebook则会出现错误信息而不是转换。所谓的“可信提供者”纯属借口。用户不会预料他们的电子邮件/密码账户被悄悄吊销。我希望谷歌能保持一致。谷歌不应该静默转换账户。当我处于“每个电子邮件一个账户”的模式时,我该如何防止这种行为? - Greg Ennis
3
我在我的应用程序中实现了appleSignIn、facbookSignIn和googleSignIn,并发现苹果和谷歌的ID没有这个问题,但只有Facebook存在这个问题。可能是因为其他两个来源被认为更可靠。 - Amit Bravo
@GregEnnis 因为有区别。Google更可信,因为他们也是一个身份验证平台。而Facebook只能验证(一次)电子邮件是否属于用户,但不能确定它是否属于用户。 - apnerve
自发布此内容以来,受信任的提供商列表已大幅扩展。请参见https://dev59.com/Obnoa4cB1Zd3GeqPN0y2 - Frank van Puffelen

15

我最终采用了以下逻辑:

如果用户尝试使用Facebook登录,但是已经存在具有相同电子邮件地址的Google帐户,并出现以下错误:

"已经存在一个具有相同电子邮件地址但不同登录凭据的帐户。请使用与此电子邮件地址相关联的提供程序进行登录。"

那么,只需要求用户使用Google登录(然后在静默情况下将Facebook链接到现有帐户)

使用Firebase的Facebook和Google登录逻辑


我认为你的实现方式可能有误?当用户登录Google账户时,请检查是否已经存在使用其他提供商(如Facebook)注册过该电子邮件的用户,如果是,则不要将用户登录到Firebase,而是显示一个错误提示,告诉他们应该使用Facebook或其他提供商。对吗? - user1056585
如果我想测试所有类型:电子邮件、Facebook、Google等,该怎么办? - Shlomo
你好,能帮忙解决这个问题吗?https://stackoverflow.com/questions/63762463/how-to-prevent-multiple-account-for-same-user-in-firebase - dReAmEr
这解决了其中一个问题。但是,当用户首先使用Facebook登录,然后使用Google登录,再次使用Facebook登录时,它并不能解决问题。Google会擦除Facebook凭据,我们需要通过再次使用Google登录来将Facebook链接到Google帐户。 - Jeff Padgett
但是我有4个提供者。当出现“FirebaseAuthUserCollisionException”异常时,我们无法知道要向用户显示哪个提供程序以进行登录。 - Muhammad Saqib

9
在Firebase中,非常重要的是在用户使用Facebook第一次登录时发送验证电子邮件以验证其电子邮件账户。
一旦电子邮件得到验证,如果用户使用的是@gamil.com作为电子邮件地址,则可以同时使用Facebook和Gmail进行登录。
引用:
Facebook登录-> 点击验证电子邮件中的链接-> Gmail登录-> Facebook登录(OK)
Facebook登录-> Gmail登录-> 点击验证电子邮件中的链接-> Facebook登录(NOT OK)
如果在用户注销并尝试使用他们的Gmail登录之前未验证Facebook电子邮件,则会在他们使用Gmail登录时无法再次使用Facebook登录。
更新 - 如果您选择始终信任Facebook电子邮件。
您可以设置Firebase函数(触发器),以便第一次登录是通过Facebook帐户进行登录时,自动将emailVerified设置为true。
示例代码。
const functions = require('firebase-functions');
const admin = require('firebase-admin');

exports.app = functions.auth.user().onCreate( async (user) => {

  if (user.providerData.find(d => d && d.providerId === 'facebook.com') || user.providerData === 'facebook.com') {
    
      try {
      await admin.auth().updateUser(user.uid, {
          emailVerified: true
        })
      } catch (err) {
        console.log('err when verifying email', err)
      }
  }
})

文档:Firebase认证触发器


这是一个很好的答案 - 在大多数情况下,这将解决问题。如果他们之后使用苹果或谷歌,但仍然很糟糕,因为如果他们不进行验证,它将被覆盖。但这是我在其他任何地方都没有找到的信息 - 谢谢。 - Alex Hartford
这并非完全正确。当用户使用Facebook登录后,若尝试使用相同电子邮件地址使用Google登录,则会出现帐户已注册的错误。如果不适当地处理该错误,则先前的Facebook登录将被覆盖。但Firebase允许您链接两个帐户,以便用户可以选择任意一个进行登录。 - I0_ol
如果您通过Facebook登录,并在使用Google帐户登录之前将emailVerified = true设置为true,则可以再次登录Facebook而不会出现错误。这正是帖子所说的。 - Someone Special

8
为了在不影响账户安全的情况下最小化登录UI点击次数,Firebase Authentication引入了“受信任提供者”的概念,其中身份提供者也是电子邮件服务提供者。例如,Google是@gmail.com地址的受信任提供者,Yahoo是@yahoo.com地址的受信任提供者,而Microsoft是@outlook.com地址的受信任提供者。
在“每个电子邮件地址一个帐户”的模式下,Firebase Authentication尝试基于电子邮件地址链接帐户。如果用户从受信任提供者登录,则我们立即知道该用户拥有该电子邮件地址,并且用户会立即登录到帐户中。
如果使用非受信任凭据(例如非受信任提供者或密码)创建具有相同电子邮件地址的现有帐户,则出于安全原因会删除先前的凭据。钓鱼者(不是电子邮件地址所有者)可能会创建初始帐户-删除初始凭据将防止钓鱼者之后访问该帐户。

17
这种行为让我疯狂。如果我们不想信任其他OAuth提供商,就不会启用它们。例如,通过启用Facebook,我相信他们已经验证了用户的电子邮件地址。这对我来说已经足够好了。我不认为在这种情况下存在钓鱼的可能性。 - Derrick Miller
2
完全同意你的看法,@DerrickMiller,谷歌在这里玩了一个非常糟糕的游戏。你找到处理异常的方法了吗? - Shreeram K

0

正如某位专家所说,Facebook用户需要进行电子邮件验证,同时需要提供电子邮件和密码(提供者=密码)。

我想,如果用户使用谷歌电子邮件,则自动获得验证状态。

应该由Google解决的问题:

如果用户通过Facebook登录,然后再通过Gmail登录,如果他没有完成电子邮件验证,则该帐户将被覆盖。稍后进行电子邮件验证也不会改变任何内容。

如果用户通过Facebook和验证电子邮件(Gmail)登录,则一切都很好。可以使用两个社交媒体提供商登录。


-1

允许使用相同的电子邮件地址创建多个帐户是您要寻找的。

仅当您在后端检查电子邮件并将其作为用户参考时,才能正常工作。如果您使用Firebase Id,则无法保持唯一用户。


2
不是因为你最终会拥有不同的帐户和不同的ID。 - Dani
是的,通常要避免使用相同的电子邮件地址创建多个帐户;这会在以后导致很多问题! - Zorayr

-1

我有同样的问题。所以我在 Firebase 控制台的“身份验证”>“用户”中删除了您用于测试登录的谷歌帐户和 Facebook 帐户。


-11
我曾经遇到过同样的问题,你只需要进入Firebase控制台,然后在“身份验证”类别中删除你想要删除的用户即可。
这对我很有效。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接