Firebase 云消息传递 - 处理注销

Posted

技术标签:

【中文标题】Firebase 云消息传递 - 处理注销【英文标题】:Firebase Cloud Messaging - Handling logout 【发布时间】:2017-08-28 19:50:04 【问题描述】:

当用户退出我的应用程序并且我不再希望他接收到设备的通知时,我该如何处理。

我试过了

FirebaseInstanceId.getInstance().deleteToken(FirebaseInstanceId.getInstance().getId(), FirebaseMessaging.INSTANCE_ID_SCOPE)

但我仍然会通过设备的registration_id 收到通知。

我还确定这是我应该删除的令牌:

FirebaseInstanceId.getInstance().getToken(FirebaseInstanceId.getInstance().getId(), FirebaseMessaging.INSTANCE_ID_SCOPE)

或者只是FirebaseInstanceId.getInstance().getToken())。

我也尝试了FirebaseInstanceId.getInstance().deleteInstanceId(),但是下次我调用FirebaseInstanceId.getInstance.getToken 时收到 null(它在第二次尝试时有效)。

我想,在deleteInstanceId 之后,我可以立即再次调用getToken(),但它看起来像个黑客。还有this answer 声明不应该这样做,但它建议删除显然不起作用的令牌。

那么处理这个问题的正确方法是什么?

【问题讨论】:

在深入实施这些解决方案之一之前,请务必查看底部的 Dan Alboteanu 答案; TL;DR 大部分应该在服务器端处理,而不是客户端。 【参考方案1】:

好的。所以我设法做了一些测试并得出以下结论:

    deleteToken()getToken(String, String) 的对应物,但不是 getToken()

仅当您传递的发件人 ID 是不同的发件人 ID(与您的 google-services.json 中可以看到的 ID 不同)时才有效。例如,您想允许不同的服务器发送到您的应用程序,您调用 getToken("THEIR_SENDER_ID", "FCM") 以授予他们授权发送到您的应用程序。这将返回一个不同的注册令牌,该令牌仅对应于该特定发件人。

以后,如果您选择删除他们的授权以发送到您的应用,那么您将不得不使用deleteToken("THEIR_SENDER_ID", "FCM")。这将使相应的令牌失效,并且当 Sender 尝试发送消息时,按照预期的行为,他们将收到 NotRegistered 错误。

    为了删除自己Sender的token,正确的处理方式是使用deleteInstanceId()

特别提到这个answer by @Prince,特别是帮助我解决这个问题的代码示例。

正如@MichałK 在他的帖子中所做的那样,在调用deleteInstanceId() 之后,应该调用getToken() 以发送对新令牌的请求。但是,您不必第二次调用它。只要实现了 onTokenRefresh() onNewToken(),它应该会自动触发为您提供新令牌。

简称deleteInstanceId() > getToken() > 勾选onTokenRefresh() onNewToken().

注意:调用deleteInstanceId() 不仅会删除您自己应用的令牌。它将删除与应用实例关联的所有主题订阅和所有其他令牌。


你确定你打电话给deleteToken() 正确吗? Audience 的值应该是(也可以从您链接的我的答案中看到)是“设置为应用服务器的发件人 ID”。您传递的 getId() 值与发件人 ID 不同(它包含应用程序实例 ID 值)。另外,您如何发送消息(应用服务器或通知控制台)?

getToken()getToken(String, String) 返回不同的令牌。见我的回答here。

我也尝试了FirebaseInstanceId.getInstance().deleteInstanceId(),但是下次我调用FirebaseInstanceId.getInstance.getToken 时收到 null(它在第二次尝试时有效)。

这可能是因为您第一次调用getToken() 时,它仍在生成中。这只是预期的行为。

我想,在deleteInstanceId 之后我可以立即再次调用getToken(),但它看起来像个黑客。

不是真的。这是您获取新生成的(假设它已经生成)令牌的方式。所以我觉得还可以。

【讨论】:

这是唯一一个在我调用 getToken 或 deleteToken 时没有抛出错误的“发件人 ID”。当我使用我的 firebase 控制台中的文本项目 ID 时,两种方法都抛出了。然后我使用了在 googleservices.json 中找到的数字 id,它看起来很有效。然后通过 getId() 并且它也没有抛出。所以我想就是这样。 至于 hack,我必须在 deleteInstanceId 之后立即调用它,所以它第一次返回 null,然后在登录时调用它才能工作。这就是为什么我认为这是一个 hack。 我会尝试看看我是否可以稍后进行一些测试并复制该行为。如果我有时间会回到这里。干杯! 感谢您的检查!老实说,我很惊讶这个 API 有多么糟糕和记录不充分……不过,我会试一试,谢谢! 有没有办法在离线时停止监听 FCM,因为在这种情况下 deleteInstanceId 会返回一个 SERVICE_NOT_AVAILABLE_ERROR,而你仍然会被注册【参考方案2】:

我做了一个简短的研究,研究什么是最优雅的解决方案,可以像以前一样重新获得完全控制(订阅和取消订阅 FCM)。在用户登录或注销后启用和禁用 FCM。

第 1 步 - 防止自动初始化

Firebase 现在可以处理 InstanceID 以及需要生成注册令牌的所有其他内容。首先你需要防止自动初始化。根据official set-up documentation,您需要将这些元数据值添加到您的androidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<application>

  <!-- FCM: Disable auto-init -->
  <meta-data android:name="firebase_messaging_auto_init_enabled"
             android:value="false" />
  <meta-data android:name="firebase_analytics_collection_enabled"
             android:value="false" />

  <!-- FCM: Receive token and messages -->
  <service android:name=".FCMService">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT"/>
    </intent-filter>
  </service>

</application>

现在您禁用了自动令牌请求过程。同时,您可以选择在运行时通过代码再次启用它。

第 2 步 - 实现 enableFCM()disableFCM() 函数

如果您再次启用自动初始化,那么您会立即收到一个新令牌,因此这是实现enableFCM() 方法的完美方式。 所有订阅信息都分配给 InstanceID,因此当您删除它时,将启动取消订阅所有主题。这样你就可以实现disableFCM()方法,在你删除它之前关闭自动初始化。

public class FCMHandler 

    public void enableFCM()
        // Enable FCM via enable Auto-init service which generate new token and receive in FCMService
        FirebaseMessaging.getInstance().setAutoInitEnabled(true);
    

    public void disableFCM()
        // Disable auto init
        FirebaseMessaging.getInstance().setAutoInitEnabled(false);
        new Thread(() -> 
            try 
                // Remove InstanceID initiate to unsubscribe all topic
                // TODO: May be a better way to use FirebaseMessaging.getInstance().unsubscribeFromTopic()
                FirebaseInstanceId.getInstance().deleteInstanceId();
             catch (IOException e) 
                e.printStackTrace();
            
        ).start();
    


第 3 步 - FCMService 实现 - 令牌和消息接收

在最后一步中,您需要接收新令牌并直接发送到您的服务器。 另一方面,您将收到您的数据消息,然后按照您的意愿去做。

public class FCMService extends FirebaseMessagingService 

    @Override
    public void onNewToken(String token) 
        super.onNewToken(token);
        // TODO: send your new token to the server
    

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) 
        super.onMessageReceived(remoteMessage);
        String from = remoteMessage.getFrom();
        Map data = remoteMessage.getData();
        if (data != null) 
            // TODO: handle your message and data
            sendMessageNotification(message, messageId);
        
    

    private void sendMessageNotification(String msg, long messageId) 
        // TODO: show notification using NotificationCompat
    

我认为这个解决方案是清晰、简单和透明的。我在生产环境中进行了测试,并且可以正常工作。希望对您有所帮助。

【讨论】:

你好 Janos,如果我没有通过调用“FirebaseMessaging.getInstance().setAutoInitEnabled(true);”来启用自动初始化有什么影响?应用是否会收到“onNewToken”回调? 哦..对不起我迟到的答案。是的,再次启用后,您通过调用 onNewToken() 在 FCMService 中获得一个新的令牌。 我认为这是一个很好的实现。还要考虑清理不再有效的令牌(在服务器端) - 当它收到 - error.code === 'messaging/invalid-registration-token' || error.code === '消息/注册-令牌-未注册'。检查此代码实验室示例github.com/firebase/friendlychat-web/blob/master/… 所以观察身份验证状态和 if(user == null) disableFCM() ... FirebaseInstanceId 已被弃用。 FirebaseMessaging.deleteToken 任务应该用于移除令牌。【参考方案3】:

当我从我的应用程序中完成我的logout() 时,我正在解决同样的问题。但问题是退出后,我仍然收到来自 Firebase 的推送通知。我尝试删除 Firebase 令牌。但是在我的logout() 方法中删除令牌后,当我在我的login() 方法中查询它时,它是null。经过2天的工作,我终于得到了解决方案。

    在您的logout() 方法中,在后台删除 Firebase 令牌,因为您无法从主线程中删除 Firebase 令牌

    new AsyncTask<Void,Void,Void>() 
        @Override
        protected Void doInBackground(Void... params) 
            try
            
                FirebaseInstanceId.getInstance().deleteInstanceId();
             catch (IOException e)
            
                e.printStackTrace();
            
            return null;
        
    
        @Override
        protected void onPostExecute(Void result) 
            // Call your Activity where you want to land after log out
        
    .execute();
    

    在您的 login() 方法中,再次生成 Firebase 令牌。

    new AsyncTask<Void,Void,Void>() 
        @Override
        protected Void doInBackground(Void... params) 
            String token = FirebaseInstanceId.getInstance().getToken();
            // Used to get firebase token until its null so it will save you from null pointer exeption
            while(token == null) 
                token = FirebaseInstanceId.getInstance().getToken();
            
            return null;
        
        @Override
        protected void onPostExecute(Void result) 
        
    .execute();
    

【讨论】:

while(token == null) busy loop 对你的电池来说真的很糟糕......最好是注册一个回调。另一个(不好但更好)是在等待令牌时执行 Thread.sleep。 @AmirUval,感谢您的建议,我一定会尝试并让您知道【参考方案4】:

开发人员绝不应将客户端应用程序取消注册为一种机制 注销或切换用户,原因如下:

注册令牌未与特定登录用户关联。如果客户端应用程序取消注册然后重新注册,该应用程序可以 接收相同的注册令牌或不同的注册令牌。

取消注册和重新注册可能需要最多五分钟才能传播。在此期间,邮件可能会被拒绝,因为 未注册状态,消息可能会发送给错误的用户。做 确保消息发送给目标用户:

应用服务器可以维护当前用户和注册令牌之间的映射。

然后,客户端应用程序可以检查以确保它收到的消息与登录用户匹配。

此引用来自已弃用的谷歌文档

但有理由相信这仍然是正确的 - 即使上面的文档已被弃用。

您可以在此处观察 - 在此代码实验室 https://github.com/firebase/functions-samples/blob/master/fcm-notifications/functions/index.js 中查看他们是如何做到的

这里 https://github.com/firebase/friendlychat-web/blob/master/cloud-functions/public/scripts/main.js

【讨论】:

您链接并引用了已弃用的 GCM 文档。我找不到 FCM 的类似信息。 我有理由相信这也适用于 FCM。看看他们在这个 codelab codelabs.developers.google.com/codelabs/… 中是如何做到的。 Codelab 已更新和维护。他们从不 deleteInstanceId()。相反,他们使用 authStateObserver(user) github.com/firebase/friendlychat-web/blob/master/… 中的令牌 - userID(uid) 对更新服务器(Firestore),同时清理服务器上不再有效的令牌 @DanAlboteanu 但您链接的代码示例未显示用户注销时会发生什么。根据我所见,服务器没有更新... 链接已失效【参考方案5】:

由于getToken()弃用,请改用getInstanceId() 重新生成新令牌。效果一样。

public static void resetInstanceId() 
    new Thread(new Runnable() 
        @Override
        public void run() 
            try 
                FirebaseInstanceId.getInstance().deleteInstanceId();
                FirebaseInstanceId.getInstance().getInstanceId();   
                Helper.log(TAG, "InstanceId removed and regenerated.");
             catch (IOException e) 
                e.printStackTrace();
            
        
    ).start();

【讨论】:

【参考方案6】:

另一种清除 firebase 令牌并使用 FirebaseMessaging.getInstance()

重新生成新令牌的便捷方法
fun clearFirebaseToken() 
    FirebaseMessaging.getInstance().apply 
        deleteToken().addOnCompleteListener  it ->
            Log.d("TAG++", "firebase token deleted $it.result")
            token.addOnCompleteListener 
                Log.d("TAG++", "firebase token generated $it.result")
                if (it.result != null) saveTokenGenerated(it.result!!)
            
        
    

【讨论】:

【参考方案7】:

只需在注销时在后台线程上调用 deleteToken 方法:

https://firebase.google.com/docs/reference/android/com/google/firebase/iid/FirebaseInstanceId.html#public-void-deletetoken-string-senderid,-string-scope

 FirebaseInstanceId.getInstance().deleteToken(getString(R.string.gcm_defaultSenderId), "FCM")

第一个参数采用 FireBaseConsole 中定义的 SenderID

更新需要几秒钟的时间 - 之后,您将不再收到 FCM 通知。

【讨论】:

【参考方案8】:

使用这种方法。 这是我的解决方案,我在here 提到了这个 注册时,使用 initFirebaseMessage,。以及注销或删除时 使用 removeFirebaseMessage()。

    private fun removeFirebaseMessage()
        CoroutineScope(Dispatchers.Default).launch 
            FirebaseMessaging.getInstance().isAutoInitEnabled = false
            FirebaseInstallations.getInstance().delete()
            FirebaseMessaging.getInstance().deleteToken()
        
    

    private fun initFirebaseMessage()
        val fcm = FirebaseMessaging.getInstance()
        fcm.isAutoInitEnabled = true
        fcm.subscribeToTopic("all")
        fcm.subscribeToTopic("")
    

【讨论】:

正在使用更新的 firebase 版本。谢谢【参考方案9】:

我知道我参加聚会迟到了。 deleteInstanceId() 应该从后台线程调用,因为它是一个阻塞调用。只需检查FirebaseInstanceId() 类中的方法deleteInstanceId()

@WorkerThread
public void deleteInstanceId() throws IOException 
    if (Looper.getMainLooper() == Looper.myLooper()) 
        throw new IOException("MAIN_THREAD");
     else 
        String var1 = zzh();
        this.zza(this.zzal.deleteInstanceId(var1));
        this.zzl();
    
  

您可以启动一个 IntentService 来删除实例 id 以及与之关联的数据。

【讨论】:

这是真的,但在这里无关紧要(其他答案也将其包装在 asynctask 中,基本相同)【参考方案10】:

包含FirebaseInstanceIdfirebase.iid 包是now deprecated。自动初始化已从 Firebase 实例 ID 迁移到 Firebase Cloud Messaging。它的行为也略有改变。以前,如果启用了自动初始化,对deleteInstanceId() 的调用将自动生成一个新令牌。现在,新令牌仅在下一次应用启动或显式调用 getToken() 时生成。

private suspend fun loginFCM() = withContext(Dispatchers.Default) 
    val fcm = FirebaseMessaging.getInstance()
    fcm.isAutoInitEnabled = true
    fcm.token.await()


private suspend fun logoutFCM() = withContext(Dispatchers.Default) 
    val fcm = FirebaseMessaging.getInstance()
    fcm.isAutoInitEnabled = false // To prevent a new token to be generated automatically in the next app-start (remove if you don't care)
    fcm.deleteToken().await()

如果您想完全退出 Firebase,您可以在之后删除整个安装:

private suspend fun logoutFirebase() = withContext(Dispatchers.Default) 
    logoutFCM()
    val firebase = FirebaseInstallations.getInstance()
    firebase.delete().await()

【讨论】:

【参考方案11】:

最后,使用后台线程删除 instanceID,下次登录时请留意 Firestore/Realtime DB(如果您将令牌保存在那里),它们会刷新

public void Logout() 

        new Thread()
            @Override
            public void run() 
                super.run();
                try 
                    FirebaseInstanceId.getInstance().deleteInstanceId();
                    FirebaseInstanceId.getInstance().getInstanceId();
                 catch (final IOException e) 
                    runOnUiThread(new Runnable() 
                        @Override
                        public void run() 
                            Toast.makeText(Flags.this, e.getMessage(), Toast.LENGTH_SHORT).show();
                        
                    );
                
            
        .start();
        FirebaseMessaging.getInstance().setAutoInitEnabled(false);
        FirebaseAuth.getInstance().signOut();
        SharedPreferences sharedPreferences = getDefaultSharedPreferences(Flags.this);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.clear();
        editor.apply();
        startActivity(new Intent(Flags.this, MainActivity.class));
        Flags.this.finish();
    

【讨论】:

【参考方案12】:

我使用了下面的这段代码,它对我有帮助,我使用 Kotlin 协程而不是 Thread(Runnable).start(),因为它比创建新线程对象的成本更低

 private fun logoutFromFCM() 
    GlobalScope.launch(Dispatchers.IO) 
        FirebaseInstallations.getInstance().delete()
        FirebaseMessaging.getInstance().deleteToken()

        FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener  task ->
            if (!task.isSuccessful) 
                Log.w(TAG, "Fetching FCM registration token failed", task.exception)
                return@OnCompleteListener
            

            // Get new FCM registration token
            val token = task.result
            saveFirebaseToken(token)
            Log.w(TAG, "Token Updated - newToken> $token")
        )
    

【讨论】:

【参考方案13】:

对于许多通知要求很简单的情况,处理注销的问题可以更容易地实现。例如,在我的例子中,每个用户只订阅了两个主题:

全球alerts话题 定义为用户电子邮件的用户特定主题(将 @ 替换为 -,因为主题字符串中不允许使用 @

对于这种简单的场景,只需在注销时取消订阅不需要的主题:

Future<void> signOut() async 
  messaging.unsubscribeFromTopic(emailToTopic(_firebaseAuth.currentUser.email));
  await _firebaseAuth.signOut();

当然,只有在成功登录或注册后才能订阅主题:

Future<String> signIn(String email, String password) async 
  try 
    await _firebaseAuth.signInWithEmailAndPassword(
        email: email, password: password);
    messaging.subscribeToTopic(emailToTopic(email));
    return "Signed in";
   on FirebaseAuthException catch (e) 
    return e.message;
  

【讨论】:

以上是关于Firebase 云消息传递 - 处理注销的主要内容,如果未能解决你的问题,请参考以下文章

如何向 FCM(Firebase 云消息传递)令牌的特定用户发送消息?

如何在 Flutter 中删除 Firebase 云消息传递令牌

用于 Flutter 的 Firebase 云消息传递 - 可选择处理后台消息错误

Flutter - Firebase 云消息传递,iOS 上未收到数据消息

Firebase 云消息传递的 AbstractMethodError

如何使用 FCM(Firebase 云消息传递)制作紧凑通知?