Firebase云消息传递 - 如何处理注销

95

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

我尝试过

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(),但这看起来像是一个hack。此外,这个答案表明不应该这样做,但它提出了删除标记,显然不起作用。

那么,正确的处理方法是什么?


2
在你开始实现这些解决方案之前,一定要先查看底部的Dan Alboteanu的答案; 简而言之,大多数情况都应该在服务器端处理,而不是客户端。 - tir38
13个回答

78

好的。所以我进行了一些测试,得出以下结论:

  1. deleteToken()getToken(String, String) 的配对方法,但不适用于 getToken()

只有在传递的 Sender ID 不同于可以在你的 google-services.json 中看到的相同 ID 时,它才起作用。例如,你想允许不同的服务器发送消息给你的应用程序,你调用 getToken("THEIR_SENDER_ID", "FCM") 来授权他们向你的应用程序发送消息。这将返回一个仅对应该特定发送者的不同注册令牌。

在将来,如果你决定撤销他们向你的应用程序发送消息的授权,那么你将需要使用 deleteToken("THEIR_SENDER_ID", "FCM")。这将使相应的令牌失效,当发送方尝试发送消息时,它将收到一个 NotRegistered 错误。

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

特别提到这个 Prince 的答案,其中包含代码示例帮助我解决了这个问题。

就像 @MichałK 在他的帖子中已经做的那样,在调用 deleteInstanceId() 后,应该调用 getToken() 来请求一个新的令牌。但是,你不必第二次调用它。只要实现了 onTokenRefresh() onNewToken(),它就应该自动触发并提供给你新的令牌。

简而言之,deleteInstanceId() > getToken() > 检查 onTokenRefresh() onNewToken()

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


您确定正在正确调用deleteToken()吗?受众值(也可以从您链接的答案中看到)应为“设置为应用程序服务器的发送者ID”。 您传递的是getId()值,这与发送者ID不同(它包含应用程序实例ID值)。此外,您是通过App Server或Notifications Console发送消息的?

getToken()getToken(String, String)返回不同的令牌。请参见我在这里的答案。

我还尝试过FirebaseInstanceId.getInstance().deleteInstanceId(),但下一次调用FirebaseInstanceId.getInstance.getToken时,我会收到null(第二次尝试时可以正常工作)。

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

我猜,在deleteInstanceId之后,我可以立即再次调用getToken(),但这看起来像一个hack。

不是真的。这就是您将获得新生成(前提是它已经生成)令牌的方式。 所以我认为没问题。


关于这个 hack,我必须在 deleteInstanceId 之后立即调用它,这样第一次它就会返回 null,然后在登录期间再次调用它才能正常工作。这就是为什么我认为这是一个 hack。 - Michał Klimczak
我会尝试并查看是否可以稍后进行一些测试并复制该行为。如果有时间回到这里。干杯! - AL.
做了一些事情。编辑了我的帖子。同时学到了新的东西。如果有不清楚的地方,请告诉我。干杯! :) - AL.
2
谢谢您的查看!老实说,我很惊讶这个API有多么糟糕和文档不完善...不过还是会试一下,谢谢! - Michał Klimczak
4
有没有办法在离线时停止监听 FCM,因为在这种情况下,deleteInstanceId 将返回 SERVICE_NOT_AVAILABLE_ERROR,而您仍将被注册。 - desgraci
显示剩余9条评论

37

我对如何以最优雅的方式恢复以前的完全控制(订阅和取消订阅FCM)进行了简要研究。在用户登录或注销后启用或禁用FCM。

第一步 - 防止自动初始化

现在Firebase处理InstanceID和生成注册令牌所需的所有其他内容。首先,您需要防止自动初始化。根据官方设置文档,您需要将这些元数据值添加到您的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();
    }

}

第三步 - 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”回调? - Hey You
哦,抱歉回复晚了。是的,在您重新启用后,通过调用onNewToken()在FCMService中获取新令牌。 - János Sicz-Mesziár
1
我认为这是一个不错的实现。还要考虑清理不再有效的令牌(在服务器端)- 当它接收到错误代码 === 'messaging/invalid-registration-token' ||           error.code === 'messaging/registration-token-not-registered'。请查看此 codelab 示例 https://github.com/firebase/friendlychat-web/blob/master/cloud-functions/functions/index.js - Dan Alboteanu
因此观察认证状态,如果用户为空,则禁用 FCM(Firebase Cloud Messaging)... - Dan Alboteanu
FirebaseInstanceId 已过时。应使用 FirebaseMessaging.deleteToken 任务来删除 Token。 - Beloo
感谢您分享代码。这应该是被接受的答案。一些方法现在已经过时了。请使用以下方法:FirebaseInstallations.getInstance().delete() FirebaseMessaging.getInstance().deleteToken() - NimaAzhd

20

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

  1. 在您的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();

3
当(token == null) { busy loop } 对你的电池非常不好...最好的方法是注册回调。另一种(不好但更好)是在等待令牌时使用Thread.sleep。 - Amir Uval
@AmirUval,谢谢您的建议,我一定会尝试并告诉您结果。 - Sunil

11
开发人员不应该注销客户端应用程序作为注销或在用户之间切换的机制,原因如下:
- 注册令牌与特定已登录用户无关。如果客户端应用程序注销然后重新注册,则应用程序可以接收相同的注册令牌或不同的注册令牌。 - 注销和重新注册可能需要长达五分钟才能传播。在此期间,由于未注册状态,可能会拒绝消息,并且消息可能会发送到错误的用户。为了确保消息发送到预期的用户: - 应用服务器可以维护当前用户和注册令牌之间的映射。 - 然后,客户端应用程序可以检查确保其接收到的消息与已登录用户匹配。
这段引文来自已废弃的谷歌文档,但有理由相信即使上面的文档已被废弃,这些内容依然是正确的。你可以在这里观察到它们是如何在 codelab https://github.com/firebase/functions-samples/blob/master/fcm-notifications/functions/index.jshttps://github.com/firebase/friendlychat-web/blob/master/cloud-functions/public/scripts/main.js 中实现的。

1
您链接和引用了已弃用的GCM文档。我无法找到类似的FCM信息。 - Michał Klimczak
1
我有理由相信这也适用于FCM。看看他们在这个codelab中是如何做的 https://codelabs.developers.google.com/codelabs/firebase-cloud-functions/index.html?index=..%2F..index#9 。该codelab已经更新和维护。他们从未删除InstanceId()。相反,他们使用authStateObserver(user)中的token-userID(uid)对在服务器(Firestore)上进行更新 https://github.com/firebase/friendlychat-web/blob/master/cloud-functions/public/scripts/main.js ,同时清除不再有效的令牌。 - Dan Alboteanu
1
@DanAlboteanu 但是你提供的代码示例没有展示用户注销时会发生什么。据我所见,服务器上没有更新... - enyo
链接已经失效。 - Vadim Kotov
客户端应用程序可以检查以确保其接收到的消息与已登录的用户匹配。这样做是非常糟糕的。客户端不应该决定他们是否有资格接收信息。这很容易被黑客攻击,从而接收所有通知,包括之前登录的用户的通知。在发送通知的云函数中应该有一种方法来检查 fcm 令牌是否有资格接收消息。 - Fred

9
自从 getToken() 被弃用后,应该使用 getInstanceId() 重新生成新的 token。它具有相同的效果。
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();
}

5

使用以下方法。 这是我的解决方案,我在这里参考了它。 当您注册时,请使用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版本。谢谢。 - Arsalan Fakhar Siddiqui

4

通过FirebaseMessaging.getInstance(),另一个方便的方法是清除Firebase令牌并生成新的令牌。

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!!)
            }
        }
    }
}

2

登出时,在后台线程上调用deleteToken方法:

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


请注意保留HTML标签。该方法是删除FirebaseInstanceId的令牌。
 FirebaseInstanceId.getInstance().deleteToken(getString(R.string.gcm_defaultSenderId), "FCM")

第一个参数是以你在FireBase控制台中定义的SenderID为准。

enter image description here

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


1

我知道我来晚了。 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和数据。

这是正确的,但在这里无关紧要(其他答案也将其包装为异步任务,基本上是相同的) - Michał Klimczak

1
firebase.iid包含FirebaseInstanceId,现在已经弃用。自动初始化已从Firebase Instance 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()
}

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