Firebase (2) - Cloud Messaging -
FirebaseのCloud Messagingを使ってみる
環境の設定
Firebase Consoleからプロジェクトを作成。その後に以下の手順をふむ
- 設定・全般からマイアプリの登録を行いapiKeyなどの必要な情報の取得(WebとAndroid両方)
- 設定・クラウドメッセージングからウェブプッシュ証明の生成(※Androidアプリ側は後述するgoogle-services.jsonを使用するので不要)
- 設定・サービスアカウントから秘密鍵を生成(※クライアントとかからWeb Pushを実行する側で必要)
マイアプリの登録
Firebase Consoleから設定・全般タブからマイアプリを登録。今回はWeb PushとAndroidの両方を使うのでこの2つを登録する

- Webフロントエンド版側は表示されてるfirebaseConfigの変数(apiKeyなどが含まれてるHash Object)の情報をコピーする
- Androidアプリ側はgoogle-services.jsonをダウンロードしてプロジェクトのappディレクトリの直下に置く
Webプッシュ証明書を設定
Firebase Configから設定・クラウドメッセージングタブからウェブプッシュ証明書を生成する。

そこに表示されてる鍵ペア値をコピーする
送信側クライアントで使用する秘密鍵を取得

Cloud Messagingを送信する側でFirebaseを使用する為にこれが必要なのだが、これを利用するときの手法が2つあって
- "firebase-admin/app"パッケージのcertを使ってこのJSONをコード中で読み込んで実行する方法
- "firebase-admin/app"パッケージのapplicationDefaultを使って環境変数(GOOGLE_APPLICATION_CREDENTIALS)からロードする方法
また、Google CloudのIAM上でWorkload Identityを設定することでこの秘密鍵のファイルをダウンロードして本番環境なのに配置したりしなくてもapplicationDefaultを使えばできるらしい(Gemini曰く)。AWSなどを使って行う場合にはこの環境変数をAWS Secret Managerを使ってセットしておいてそれをクライアントのプログラム上から使用するような感じでもできるらしい。だけどAWS使うなら素直にWorkload Identityを設定した方がきっと楽な気がする
これで設定関係は終わり
権限の確認
Google CloudのIAM管理からCloud Messagingの権限管理で

っていう感じで上記の秘密鍵のclient_emailで指定されてあるメールアドレスとプリンシパルが一致してるか確認。まぁ大体ちゃんとそうなってるはず
さてここからコーディングを
Web側(index.html)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>FCMテスト</title>
<script src="https://www.gstatic.com/firebasejs/12.15.0/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/12.15.0/firebase-messaging-compat.js"></script>
</head>
<body>
<div id="token-area"></div>
<script>
const firebaseConfig = {
// 省略
};
firebase.initializeApp(firebaseConfig);
const messaging = firebase.messaging();
async function initFCM() {
if (!('serviceWorker' in navigator)) {
console.error('このブラウザは Service Worker をサポートしていません。');
return;
}
try {
await navigator.serviceWorker.register('./firebase-messaging-sw.js');
const swRegistration = await navigator.serviceWorker.ready;
const permission = await Notification.requestPermission();
if (permission === 'granted') {
const token = await messaging.getToken({
vapidKey: 'ウェブプッシュ証明書で取得した鍵ペアの値',
serviceWorkerRegistration: swRegistration
});
if (token) {
const tokenArea = document.getElementById('token-area');
tokenArea.innerText = token;
} else {
console.warn('トークンが取得できませんでした');
}
} else {
console.warn('通知がブロックされています。');
}
} catch (error) {
console.error('エラーが発生しました:', error);
}
}
window.addEventListener('load', initFCM);
messaging.onMessage((payload) => {
alert(`【通知受信】\nタイトル: ${payload.notification.title}\n本文: ${payload.notification.body}`);
});
</script>
</body>
</html>
firebaseConfigに登録したマイアプリのWebタイプのapiKeyなどが含まれるのをここにコピーする。もちろんServiceWorker側も必要なので作っていく
Web側(firebase-messaging-sw.js)
importScripts('https://www.gstatic.com/firebasejs/12.15.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/12.15.0/firebase-messaging-compat.js')
const firebaseConfig = {
// 省略
};
firebase.initializeApp(firebaseConfig)
firebase.messaging();
本当はWeb側のJSとServiceWorkerでfirebaseConfigを共通化できるようにするべきなんだろうけど
Webサーバーにデプロイしてからこれにアクセスすると画面上にアクセストークンが出るのでそれをコピーしておく
とりあえずちゃんと受信できるかクライアント側を作って実行して送ってみる
クライアント側(client.mjs)
import { initializeApp, applicationDefault } from 'firebase-admin/app';
import { getMessaging } from 'firebase-admin/messaging';
initializeApp({ credential: applicationDefault() });
const message = {
notification: { title: '送信', body: 'Hello' },
token: '取得したアクセストークン'
};
getMessaging().send(message).then((response) => {
console.log('送信成功しました!メッセージID:', response);
}).catch((error) => {
console.error('送信エラー:', error);
});
applicationDefaultを使用するため、GOOGLE_APPLICATION_CREDENTIALSの環境変数にダウンロードした秘密鍵.jsonのパスを指定する
GOOGLE_APPLICATION_CREDENTIALS=secret-key.json node client.mjs
これを実行するとブラウザでアクセスしてる場合はブラウザの通知でメッセージが表示される(今回の場合はalertが起きる)。ブラウザがforegroundではない(最小化してるなど)の場合にはOSの通知機能でメッセージが表示されるようになる
ついでにapplicationDefaultを使用しないでファイルを読み込んで使う場合には
import { initializeApp, cert } from 'firebase-admin/app';
import { getMessaging } from 'firebase-admin/messaging';
import { readFileSync } from 'fs';
const serviceAccount = JSON.parse(readFileSync(new URL('./secret-key.json', import.meta.url)));
initializeApp({ credential: cert(serviceAccount) });
const message = {
notification: { title: '送信', body: 'Hello' },
token: '取得したアクセストークン'
};
getMessaging().send(message).then((response) => {
console.log('送信成功しました!メッセージID:', response);
}).catch((error) => {
console.error('送信エラー:', error);
});
という感じでCloud Messagingで通知を送ったりできる。ちなみに後述するAndroidアプリの場合にはtopicSubscripbeっていうこのトピックに接続してる側一括で送信する機能があるのでわざわざアクセストークンを取得してごちゃごちゃする必要はない。しかしWeb版のAPIにはこのtopicSubscribeに該当するAPIが無いので、ユーザーのアクセストークンをDBなどに保存するなどで保管して通知を行う場合にはこのトークンをDBなどから取得して送信処理をするっていうのが必要になる(余談参照)
まぁWebアプリ版のはこんくらいでAndroidアプリでも受信してみる
Firebaseを使うAndroidアプリの環境を設定
まずAndroidアプリにFirebase関係のライブラリやらGoogle Serviceに関わるプラグインの設定やらが必要になるので以下をやっていく
/build.gradleの設定
plugins {
id 'com.android.application' version '8.2.2' apply false
id 'org.jetbrains.kotlin.android' version '1.9.22' apply false
id 'com.google.gms.google-services' version '4.4.1' apply false
}
んでお次はapp/build.gradleを
/app/build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.google.gms.google-services'
}
android {
namespace 'sample.app'
compileSdk 34
defaultConfig {
applicationId "sample.app"
minSdk 26
targetSdk 34
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation platform('com.google.firebase:firebase-bom:32.7.0')
implementation 'com.google.firebase:firebase-messaging-ktx'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}
applicationIdなどはFirebase ConsoleでAndroidアプリを登録する際に指定したアプリケーションIDのパッケージ名をちゃんと指定する
んで設定のときにダウンロードした"google-services.json"をappディレクトリの直下に入れておく
Service側で受け取って処理するためAndroidManifest.xmlも設定する
/app/src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".MyFirebaseMessagingService" android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>
あとはFirebase Cloud Messagingに送信されたデータを受信するためのServiceを作る
/app/src/main/java/.../MyFirebaseMessagingService.kt
package sample.app
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
class MyFirebaseMessagingService : FirebaseMessagingService() {
companion object {
private const val CHANNEL_ID = "fcm_topic_channel"
private const val CHANNEL_NAME = "FCMトピック通知"
private const val NOTIFICATION_ID = 1001
}
override fun onNewToken(token: String) {
super.onNewToken(token)
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
val title = remoteMessage.notification?.title ?: remoteMessage.data["title"] ?: "新しいメッセージ"
val body = remoteMessage.notification?.body ?: remoteMessage.data["body"] ?: ""
showNotification(title, body)
}
private fun showNotification(title: String, body: String) {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
createNotificationChannel(notificationManager)
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
}
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE)
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle(title)
.setContentText(body)
.setStyle(NotificationCompat.BigTextStyle().bigText(body))
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.build()
notificationManager.notify(NOTIFICATION_ID, notification)
}
private fun createNotificationChannel(manager: NotificationManager) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH).apply {
description = "FCMトピックから受信した通知チャンネル"
}
manager.createNotificationChannel(channel)
}
}
}
まぁonMessageReceivedで受け取ってそれをNotificationで通知として出してるだけ。ただしここはあくまで受け取るだけ。トピックへの接続などはActivity側でやる
/app/src/main/java/.../MainActivity.kt
package sample.app
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.google.firebase.messaging.FirebaseMessaging
class MainActivity : AppCompatActivity() {
companion object {
const val TOPIC = "test-topic"
}
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
val msg = if (isGranted) {
"通知の許可が付与されました"
} else {
"通知の許可が拒否されました"
}
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
updateStatus()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
askNotificationPermission()
subscribeToTopic()
updateStatus()
}
private fun askNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED
) {
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
}
private fun subscribeToTopic() {
FirebaseMessaging.getInstance().subscribeToTopic(TOPIC)
.addOnCompleteListener { task ->
val msg = if (task.isSuccessful) {
"トピック「$TOPIC」にサブスクライブしました"
} else {
"サブスクライブ失敗: ${task.exception?.message}"
}
Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
updateStatus()
}
}
private fun updateStatus() {
val permissionStatus = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED
) "✓ 付与済み" else "✗ 未許可"
} else {
"✓ 不要 (Android 12以下)"
}
findViewById<TextView>(R.id.tv_status).text = """
通知権限: $permissionStatus
""".trimIndent()
}
}
"test-topic"っていうFirebase Cloud Messagingのチャンネル(クライアント側とも合わせる)を指定して、クライアントから送られたメーセージを受信できるようにする。ちなみにAndroid13以降からはこういう通知を受け取る許可をアプリ側で承認するようにしないといけないっぽい
まぁこれでAndroidアプリ側で受信できるはずなのでさっきのclient.mjsを以下のように修正して再実行
import { initializeApp, applicationDefault } from 'firebase-admin/app';
import { getMessaging } from 'firebase-admin/messaging';
initializeApp({ credential: applicationDefault() });
const message = {
notification: { title: '送信', body: 'Hello' },
topic: "test-topic"
};
getMessaging().send(message).then((response) => {
console.log('送信成功しました!メッセージID:', response);
}).catch((error) => {
console.error('送信エラー:', error);
});
ここでActivityで指定したトピック名(test-topic)を指定して送信。アプリを入れてる側で通知が表示されるようになる(はず)
というようにAndroidなどのスマホアプリ向けのはこのトピックっていう機能を使ってそれに接続してる人に通知を送信したりすることができる。ただしWeb版のAPIにはこれが無いのでそれをやるにはWeb版はアクセストークンの保持が必要になるっていう感じ(余談参照)
以上!
余談
フロントエンドにはsubscribeToTopicのようなAPIはないが、node.jsなどを利用したサーバープログラム上などではできるらしい。その手法を使って以下のようにすればWeb側でもsubscribeToTopicのようなことはできるらしい
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>FCMテスト</title>
<script src="https://www.gstatic.com/firebasejs/12.15.0/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/12.15.0/firebase-messaging-compat.js"></script>
</head>
<body>
<script>
const firebaseConfig = {
// 省略
};
firebase.initializeApp(firebaseConfig);
const messaging = firebase.messaging();
async function initFCM() {
if (!('serviceWorker' in navigator)) {
console.error('このブラウザは Service Worker をサポートしていません。');
return;
}
try {
await navigator.serviceWorker.register('./firebase-messaging-sw.js');
const swRegistration = await navigator.serviceWorker.ready;
const permission = await Notification.requestPermission();
if (permission === 'granted') {
const token = await messaging.getToken({
vapidKey: 'ウェブプッシュ証明書で取得した鍵ペアの値',
serviceWorkerRegistration: swRegistration
});
if (token) {
await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: token,
topic: 'test-topic'
})
});
} else {
console.warn('トークンが取得できませんでした');
}
} else {
console.warn('通知がブロックされています。');
}
} catch (error) {
console.error('エラーが発生しました:', error);
}
}
window.addEventListener('load', initFCM);
messaging.onMessage((payload) => {
alert(`【通知受信】\nタイトル: ${payload.notification.title}\n本文: ${payload.notification.body}`);
});
</script>
</body>
</html>
これがフロントエンドでNode.jsでexpressとfirebaseを使って以下のようなサーバーを作る
import express from 'express';
import { initializeApp, applicationDefault } from 'firebase-admin/app';
import { getMessaging } from 'firebase-admin/messaging';
initializeApp({ credential: applicationDefault() });
const app = express();
const PORT = 3000;
app.use(express.json());
app.use(express.static('.'));
app.post('/api/subscribe', (req, res) => {
const { token, topic } = req.body;
if (!token || !topic) {
return res.status(400).json({ error: 'トークンとトピック名は必須です。' });
}
getMessaging().subscribeToTopic([token], topic)
.then((response) => {
res.json({
success: true,
message: `トピック「${topic}」に登録しました。`,
details: response
});
})
.catch((error) => {
console.error('トピック登録エラー:', error);
res.status(500).json({ error: 'トピックの登録に失敗しました。', details: error.message });
});
});
app.listen(PORT, () => {
console.log(`http://localhost:${PORT}`);
});
というようにすればWebフロントエンド側でもsubscribeToTopicのような感じにすることもできるよってことでw