【问题标题】:Problem of "PERMISSION_DENIED" on Firestore get due to security rule由于安全规则,Firestore 上出现“PERMISSION_DENIED”问题
【发布时间】:2019-12-15 04:48:23
【问题描述】:

如果用户必须注册,我的 Android Java 应用程序首先使用 AuthUI.getInstance() 创建一个 Firebase 帐户(带有密码的电子邮件)。创建帐户后,会出现一个对话框,通知用户他收到了一封验证电子邮件,他必须单击电子邮件中的验证链接。用户完成此操作后,他可以关闭对话框并继续在 Firestore 中配置他的帐户。

但对 Firestore 文档的所有请求都受安全规则保护,例如

allow read: if request.auth.uid != null && request.auth.token.email != null && request.auth.token.email_verified == true;

失败

com.google.firebase.firestore.FirebaseFirestoreException: PERMISSION_DENIED: 权限缺失或不足

如果用户关闭应用程序,重新启动它并重新验证,那么它就可以工作(Firestore 请求没有权限问题)。

我做了几个测试。如果我将安全规则更改为

allow read: if request.auth.uid != null && request.auth.token.email != null;

一切正常,但从我的角度来看,它的安全性较低,因为无法保证电子邮件已通过验证。 Firestore 似乎仍然不知道该帐户已通过验证。

这是一个 Activity 示例:

package foo;

import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import com.firebase.ui.auth.AuthUI;
import com.google.android.gms.tasks.Task;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.firebase.Timestamp;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.QuerySnapshot;
import com.google.firebase.firestore.Source;
import com.google.firebase.functions.FirebaseFunctions;
import com.google.firebase.functions.HttpsCallableReference;
import com.google.firebase.functions.HttpsCallableResult;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

import fr.cinfo.planartis.R;

public class ExampleActivity extends AppCompatActivity {
    private static final int SIGN_IN_REQUEST_CODE = 123;
    private static final String VERIFICATION_EMAIL_SENT_TIMESTAMP_KEY = "verificationEmailSentTimestamp";
    private FirebaseAuth firebaseAuth;
    private FirebaseFirestore firestore;

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

        firebaseAuth = FirebaseAuth.getInstance();
        firestore = FirebaseFirestore.getInstance();

        if (firebaseAuth.getCurrentUser() == null) {
            startUserSignInActivity();
        } else {
            performEmailVerification();
        }
    }

    void startUserSignInActivity() {
        startActivityForResult(
            AuthUI.getInstance()
                .createSignInIntentBuilder()
                .setAvailableProviders(Collections.singletonList(new AuthUI.IdpConfig.EmailBuilder().build()))
                .setIsSmartLockEnabled(false, true)
                .setTosAndPrivacyPolicyUrls("https://localhost/terms.html", "https://localhost/privacy.html")
                .build(),
            SIGN_IN_REQUEST_CODE);
    }

    @Override
    protected void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
        if (requestCode == SIGN_IN_REQUEST_CODE) {
            if (resultCode != RESULT_OK) { // ERRORS
                // ... do something
                finish();
                return;
            }
            performEmailVerification();
        } else {
            super.onActivityResult(requestCode, resultCode, data);
        }
    }

    void performEmailVerification() {
        if (firebaseAuth.getCurrentUser().isEmailVerified()) {
            // Everything is OK
            checkSomethingOnFirestore();
            return;
        }

        final DocumentReference documentReference = firestore.document("users/" + firebaseAuth.getCurrentUser().getEmail());
        documentReference.get(Source.DEFAULT).addOnCompleteListener((Task<DocumentSnapshot> task) -> {
            if (task.isSuccessful()) {
                final DocumentSnapshot userSnapshot = task.getResult();
                if (userSnapshot.exists()) {
                    // Check if the first verification email was sent
                    final Object timestamp = userSnapshot.get(VERIFICATION_EMAIL_SENT_TIMESTAMP_KEY);
                    if (timestamp == null) {
                        firebaseAuth.getCurrentUser().sendEmailVerification().addOnCompleteListener((Task<Void> validationEmailTask) -> {
                            if (validationEmailTask.isSuccessful()) {
                                final Timestamp now = Timestamp.now();
                                documentReference.update(VERIFICATION_EMAIL_SENT_TIMESTAMP_KEY, now).addOnCompleteListener((Task<Void> updateUserAccountTask) -> {
                                    if (!updateUserAccountTask.isSuccessful()) {
                                    }
                                    displayWarningAboutValidation();
                                });
                            }
                            else {
                                // email not sent, so display message
                                FirebaseAuth.getInstance().signOut();
                                displayInformationDialog(this, "Unable to send a verification email", null).show();
                                finish();
                            }
                        });
                    }
                    else {
                        displayWarningAboutValidation();
                    }
                }
                else {
                    Toast.makeText(this, "We are finalizing your account creation\nPlease wait a few seconds", Toast.LENGTH_LONG).show();
                    final HttpsCallableReference httpsCallableReference = FirebaseFunctions.getInstance().getHttpsCallable("finalizeUserAccount");
                    httpsCallableReference.setTimeout(15, TimeUnit.SECONDS);
                    httpsCallableReference.call(firebaseAuth.getCurrentUser().getEmail()).continueWith((Task<HttpsCallableResult> task12) -> {
                        if (!task12.isSuccessful()) {
                            displayInformationDialog(this, "The finalization of your account failed.", (DialogInterface dialog, int id) -> {
                                FirebaseAuth.getInstance().signOut();
                                finish();
                            }).show();
                            return null;
                        }

                        displayInformationDialog(this, "A new verification email was sent", (final DialogInterface dialog, final int id) -> {
                            // Reload current user
                            firebaseAuth.getCurrentUser().reload().addOnCompleteListener((Task<Void> task1) -> {
                                if (task1.isSuccessful()) {
                                    performEmailVerification();
                                }
                                else {
                                    FirebaseAuth.getInstance().signOut();
                                    finish();
                                }
                            });

                        });
                        return null;
                    });
                }
            }
            else {
                displayInformationDialog(this, "Problem with the server", (DialogInterface dialog, int id) -> {
                    FirebaseAuth.getInstance().signOut();
                    finish();
                }).show();
            }
        });
    }

    private void checkSomethingOnFirestore() {
        firestore.collection("users").document(firebaseAuth.getCurrentUser().getEmail()).collection("documents").get().addOnCompleteListener(this, (Task<QuerySnapshot> task) -> {
            if (!task.isSuccessful()) {  // <======================================== PERMISSION_DENIED exception !!!!!!
                displayInformationDialog(this, "Problem on Firestore", (final DialogInterface dialog, final int id) -> {
                    FirebaseAuth.getInstance().signOut();
                    finish();
                })
                .show();
                return;
            }
            // Go on and do the job: for instance display the GUI or anything else
        });
    }

    private void displayWarningAboutValidation() {
        new AlertDialog.Builder(this)
            .setCancelable(false)
            .setMessage("Read the verification email we sent and click on the link inside the email")
            .setPositiveButton("I understand", (DialogInterface dialog, int id) -> firebaseAuth.getCurrentUser().reload().addOnCompleteListener((Task<Void> task) -> {
                if (task.isSuccessful()) {
                    performEmailVerification();
                }
                else {
                    FirebaseAuth.getInstance().signOut();
                    finish();
                }
            }))
            .setNeutralButton("Send back a verification email", (final DialogInterface dialog, final int which) -> firebaseAuth.getCurrentUser().sendEmailVerification().addOnCompleteListener((final Task<Void> task) -> {
                dialog.dismiss();
                if (task.isSuccessful()) {
                    // email sent
                    displayInformationDialog(this, "A new verification email was sent", (final DialogInterface dialog12, final int which12) -> {
                        FirebaseAuth.getInstance().signOut();
                        finish();
                    }).show();

                    firebaseAuth.getCurrentUser().reload().addOnCompleteListener((Task<Void> task1) -> {
                        if (task1.isSuccessful()) {
                            performEmailVerification();
                        }
                        else {
                            FirebaseAuth.getInstance().signOut();
                            finish();
                        }
                    });
                }
                else {
                    // email not sent, so display message
                    displayInformationDialog(this, "Unable to send a new verification email", (final DialogInterface dialog1, final int which1) -> {
                        FirebaseAuth.getInstance().signOut();
                        finish();
                    }).show();
                }
            }))
            .show();
    }

    private AlertDialog displayInformationDialog(final Context context, final CharSequence message, final DialogInterface.OnClickListener positiveButtonOnclickListener) {
        return new MaterialAlertDialogBuilder(context).setCancelable(false).setMessage(message).setPositiveButton("I understand", positiveButtonOnclickListener).setTitle("Planartis").setIcon(R.drawable.ic_logo_toolbar).show();
    }

}

Firestore 的行为是否正确?为了避免重新启动应用程序和重新验证(用户友好性),我可以进行哪些更改?

【问题讨论】:

    标签: java android google-cloud-firestore firebase-security


    【解决方案1】:

    根据谷歌:

    Firebase 安全规则介于您的数据和恶意用户之间。您可以编写简单或复杂的规则,将您的应用数据保护到特定应用所需的粒度级别。

    Firebase 安全规则利用可扩展、灵活的配置语言来定义您的用户可以访问哪些数据以用于实时数据库、Cloud Firestore 和 Cloud Storage。 Firebase 实时数据库规则在规则定义中利用 JSON,而 Cloud Firestore 安全规则和用于 Cloud Storage 的 Firebase 安全规则利用一种独特的语言来适应更复杂的规则特定结构。

    这是您的主要问题:如果用户未通过帐户验证,您将执行一些与 Firestore 相关的任务。但是,如果他未通过验证,您将再次收到来自 Firestore 的电子邮件。但是您的安全规则描述了您不应该能够访问数据库,除非您经过帐户验证。这就是代码失败的地方。这是你的错误:

    void performEmailVerification() {
        if (firebaseAuth.getCurrentUser().isEmailVerified()) {
            // Everything is OK, perform your task
            checkSomethingOnFirestore();
            return;
        }
        
        //User not verified, but you still get a database reference and try to get the email.
        final DocumentReference documentReference = ... //Error produced 
    

    您需要先进行验证,确认用户已通过验证,然后尝试访问数据库。

    另一个因素可能是您需要刷新您的 firebase 帐户对象,如下所示:

    FirebaseAuth.getInstance().getCurrentUser().reload();
    

    这样做是刷新您的帐户状态。因为 firebase 保留对您帐户的引用并且该引用被缓存,所以重新加载您的帐户是一个有用的想法,因此它会被更新。在if 语句之前执行此操作,以检查您的用户是否已通过验证。

    您需要查看文档:Firebase Security Rules。在那里你会发现如何编写安全规则(语法),如何不编写不安全的规则以及如何部署它们。

    【讨论】:

    • 感谢您的回答。您的解释是正确的,但这是因为我将问题简单化了,没有看到我创建的缺点。事实上,我在测试中更改了规则,以允许未经验证的电子邮件在 performEmailVerification 中执行 Firestore 请求。但是当我在 checkSomethingOnFirestore() 中执行 Firestore 请求时,也会出现权限被拒绝的问题。
    • 即使您验证了您的电子邮件,也会出现该错误?
    • 是的,在 checkSomethingOnFirestore() 中执行请求之前,我使用调试器检查了电子邮件是否经过验证
    • 我不完全确定,因为我对项目的代码和规则进行了一些更改,但它似乎在几周前起作用了
    • 发现你的问题,我想。我要编辑我的代码。
    猜你喜欢
    • 2019-08-14
    • 1970-01-01
    • 2021-01-16
    • 2019-05-05
    • 2019-08-20
    • 2020-08-14
    • 2018-10-19
    • 2019-07-05
    • 2019-02-06
    相关资源
    最近更新 更多