Web Push APIでサイトにアクセスしたユーザーにプッシュ通知する

掲載日
更新日

自分に通知するだけの実装

サービスワーカー(Service Worker)を使うと、Webアプリでネイティブアプリのようなことが出来ます。

プッシュAPIを利用することで、アプリのインストール不要(IOSはインストール必要かも)で利用者の端末にプッシュ通知を送信することが出来ます。まずは自分自身に通知するだけの方法が以下になります。

 

以下の3ファイルを任意のディレクトリ上に配置し、Apache等を使ってlocalhosthttpsで操作してください。
(サーバーを用意したり、ngrok等を使わなくてもオレオレ証明書で動きます。)

  • index.html
    サービスワーカーの登録と、通知を許可してもらうページを作成します。
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width" />
    <title>Service worker demo</title>
</head>

<body>

    <p>This is Service Worker DEMO</p>

    <button id="grant">通知の許可/拒否を設定する</button>

    <button id="send">通知を送信する</button>

    <script type="module" src="app.js"></script>
</body>

</html>

 

  • serviceWorker.js
    このjsがユーザーのローカル(ブラウザの内部?)に配置されるようになります(という事だと思う)。
    今回は下記app.jspostMessageを受け取って通知を表示するだけの処理です。
// 通知を作成する
self.addEventListener('message', function (event) {
    self.registration.showNotification(event.data);
});

 

  • app.js
    以下の処理を実行するjsを追加します。
    • サービスワーカーを登録する
    • 通知を許可する
    • 通知を送る
// ★1
console.log("サービスワーカー登録")
// 以下の記述で、serviceWorker.jsをローカル端末上で動かせる…という事だと思う。
var registration = await navigator.serviceWorker.register(
    'serviceWorker.js',
    {
        scope: './',
    }
);

// ★2
document.getElementById('grant').addEventListener('click', () => {
    let permission = Notification.permission;
    console.log("現在の通知許可状態: " + permission)
    if (permission == 'granted') {
        window.alert("許可されています。変更する場合ブラウザの設定から設定し直してください。")
    } else if (permission == 'denied') {
        window.alert("拒否されています。変更する場合ブラウザの設定から設定し直してください。")
    }

    Notification.requestPermission();
});

// ★3
document.getElementById('send').addEventListener('click', () => {
    if (Notification.permission === 'granted') {
        registration.active.postMessage('WebPushテストです。');
    } else {
        alert("通知を許可されていません。")
    }
});

まずは★1の処理でサービスワーカーを登録します。
第一引数にスクリプトのパスを指定すると、そのスクリプトをローカルに(というかブラウザ内部に)配置することが出来るという事だと思います。

ちょっとびっくりなんですが、プッシュ通知はユーザーの許可が必要ですが、サービスワーカー登録自体はユーザーの許可が不要みたいです。

 

続いて★2の処理で、「通知の許可を取得する」ボタンをクリックで、Notification.requestPermission関数を実行します。すると下図のようなダイアログが表示され、通知の許可 or 拒否を設定できます。

 

これもびっくりなんですが、一度このダイアログで許可 or 拒否すると、同じ関数を実行してもダイアログは二度と表示されません。ブラウザの設定から、権限をリセットすると再度表示されるようになります。

許可から拒否への変更に関しては、通知が来た際に拒否設定が出来るようになっているのでいいんですが、うっかり拒否してやっぱり通知欲しいケースもあると思うのですが…。
(許可されるまでページアクセスの度にダイアログ表示してくる開発者は絶対にいるので悩ましいところですが…。)

 

最後に★3の処理で、許可を得た状態で「通知を送信する」ボタンをクリックすると通知が送れるようになります。

サーバーから端末にプッシュ通知を送り付ける方法

こちらが本題です。大抵の場合、一度許可をしたユーザーがブラウザなどを閉じた後も、許可したユーザー全員宛にプッシュ通知を送りたいです。

その場合、セキュリティのために以下の追加処理と設定が必要となります。

VAPID鍵を作成する

opensslコマンドでも作れるみたいですが、npxを使って作るのが楽です。

$ npx web-push generate-vapid-keys
=======================================

Public Key:
BHYMdrh96P7BE267WHB_qfuLn1A7dqaYlPPC0JMkJvHfPWAxyUhMb5BkOgeCDsyvooXF9_EjrNefyksLxH-dd6g

Private Key:
zlg43cXH9WzmVV4qpb5t1gPFjQPhBIk64roLJmdpIcg
# ※ お試しなので書いちゃってますが、当然ですがPrivate Keyは一般には見えないようにしましょう。
=======================================

後で使用するのでどこかに控えておきましょう。

 

ユーザーの認証情報などを保存する

ユーザーに通知を許可してもらった際に以下の情報を取得してサーバーに保管します。

  • endpoint  url : サーバー → 個人の端末に通知するためのエンドポイントURL。
  • p256dh : ユーザーの公開鍵
  • auth : ユーザーの秘密鍵

 

app.jsを以下に書き換えます。

console.log("サービスワーカー登録")
var registration = await navigator.serviceWorker.register(
    'serviceWorker.js',
    {
        scope: './',
    }
);

document.getElementById('grant').addEventListener('click', () => {
    let permission = Notification.permission;
    console.log("現在の通知許可状態: " + permission)
    if (permission == 'granted') {
        window.alert("既に許可されています。ブラウザの設定から設定し直してください。")
        // ★追記
        getUserAuth();
    } else if (permission == 'denied') {
        window.alert("既に拒否されています。ブラウザの設定から設定し直してください。")
    }

    // ★修正ここから
    Notification.requestPermission().then(function () {
        if (Notification.permission == 'granted') {
            getUserAuth();
        }
    });
    // ★ここまで
});

// ★以下追加
function urlBase64ToUint8Array(base64String) {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
    const rawData = atob(base64);
    const outputArray = new Uint8Array(rawData.length);
    for (let i = 0; i < rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
}

async function getUserAuth() {
    // ★ 発行したVAPIDのPublic Key(公開鍵)の方
    const key = 'BHYMdrh96P7BE267WHB_qfuLn1A7dqaYlPPC0JMkJvHfPWAxyUhMb5BkOgeCDsyvooXF9_EjrNefyksLxH-dd6g';
    const serverPublicKey = urlBase64ToUint8Array(key)

    const subscription = await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: serverPublicKey
    });
    const sub_key = subscription.getKey('p256dh');
    const sub_token = subscription.getKey('auth');

    fetch('./register.php', {
        method: "POST",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify({
            endpoint: subscription.endpoint,
            userPublicKey: btoa(String.fromCharCode.apply(null, new Uint8Array(sub_key))),
            userAuthToken: btoa(String.fromCharCode.apply(null, new Uint8Array(sub_token)))
        })
    }).then(function (data) {
        console.log(data)
    }).catch(function (err) {
        console.log(err)
    });
}

// ここはもう消してもいい
// document.getElementById('send').addEventListener('click', () => {
//     if (Notification.permission === 'granted') {
//         registration.active.postMessage('WebPushテストです。');
//     } else {
//         alert("通知を許可されていません。")
//     }
// });
  • serviceWorker.jsに追記します。
// 以下は消してもいい
// self.addEventListener('message', function (event) {
//    self.registration.showNotification(event.data);
// });

// ★ここから追記: 受け取ったデータを使って通知を表示
self.addEventListener('push', function (event) {
    self.registration.showNotification(event.data.text());
});

messageイベントを受け取っていたのを、pushイベントに変更します。

サーバー側の処理を実装する

サーバー側の保存処理

今回はPHPを使って、register.phpを追加します。

<?php
// endpointとuserAuthとtokenを保持する

$input = file_get_contents('php://input');
$json = json_decode($input, true);

$endPoint = $json["endpoint"];
$userPublicKey = $json["userPublicKey"];
$userAuthToken = $json["userAuthToken"];

// 適当にtxtに保存 (※ 実際はDBとかに保存してください。)
$save = file_get_contents("./save.txt");

$saveJson = json_decode($save, 'true');

$saveJson[$userAuthToken] = [
    'endPoint' => $endPoint,
    'userPublicKey' => $userPublicKey,
];

file_put_contents("./save.txt", json_encode($saveJson));

これで、index.htmlをブラウザで(httpsで)開き、「通知の許可/拒否を設定する」ボタンをもう一度クリックすると、save.txtファイルにユーザー毎のendpointURLと、認証情報が保存されます。
(環境によってはregister.phpに適切な権限を与えてください。)

serviceWorker.jsが更新されていない可能性もあるので、後述の操作をしてみてpush通知が送れない場合は下図の方法で開発者ツールから更新します。

 

手動でサービスワーカーを新しくする場合、index.htmlを開き、F12キー等で開発者ツールを開き、アプリケーションタブを開き、「Service workers」項目で更新ボタンをクリックします。


(実際の運用では、登録されているか確認→ unregister → 再登録みたいなことをする必要がありそう。)

プッシュ通知送信側の処理

独立している処理なので、save.txtが閲覧できるなら別ディレクトリに作成しても構いません。

まず以下のコマンドで必要なライブラリをインストールします。

composer require minishlink/web-push
  • push.phpを追加します。
<?php
require_once 'vendor/autoload.php';

use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;

const VAPID_SUBJECT = 'https://XXXX'; // Push通知しているサイトのURL
// ★ 発行したVAPIDのPublic Key(公開鍵)の方
const PUBLIC_KEY = 'BHYMdrh96P7BE267WHB_qfuLn1A7dqaYlPPC0JMkJvHfPWAxyUhMb5BkOgeCDsyvooXF9_EjrNefyksLxH-dd6g';
// ★ 発行したVAPIDのPrivate Key(秘密鍵)の方
const PRIVATE_KEY = 'zlg43cXH9WzmVV4qpb5t1gPFjQPhBIk64roLJmdpIcg';

$saveData = file_get_contents(__DIR__ . "/../save.txt");
$json = json_decode($saveData, true);

foreach ($json as $userAuthToken => $item) {
    // push通知認証用のデータ
    $subscription = Subscription::create([
        'endpoint' => $item["endPoint"],
        'publicKey' => $item["userPublicKey"],
        'authToken' => $userAuthToken,
    ]);

    // ブラウザに認証させる
    $auth = [
        'VAPID' => [
            'subject' => VAPID_SUBJECT,
            'publicKey' => PUBLIC_KEY,
            'privateKey' => PRIVATE_KEY,
        ]
    ];

    $webPush = new WebPush($auth);

    $report = $webPush->sendOneNotification(
        $subscription,
        'ただいまの時刻は' . date("Y-m-d H:i:s") . 'です。'
    );

    $endpoint = $report->getRequest()->getUri()->__toString();

    if ($report->isSuccess()) {
        echo '送信成功!';
    } else {
        echo '送信失敗!';
    }
}

これでphp push.phpを実行するとプッシュ通知が送られてきます。

 

ChromeブラウザでWebPushAPIを許可して、Push通知を送ってみたスクリーンショット。
ChromeのPush通知

 

FireFoxブラウザでWebPushAPIを許可して、Push通知を送ってみたスクリーンショット。
FireFoxのPush通知

まとめ

サービスワーカーを使うと、今回のPush通知のようにWebサイトだけでネイティブアプリのようなことを出来るので非常に便利です。

ただ、今回調べてみてちょっと色々仕様が心配でした。サービスワーカーの登録が勝手に出来るのは容量爆発しそうですが良いんでしょうか。ブラウザ毎に仕様が異なるのもイマイチに感じます。
(今回の方法だとIOSではプッシュ通知出来ないらしい。ホーム画面にアイコンを追加する必要があるらしいですが、Androidユーザーなので確認できず。)

記事の作成者のA.W.のアイコン

この記事を書いた人

A.W.
茨城県在住Webエンジニアです。 PHP、PostgreSQL、Linuxなどを業務で使用しています。 趣味ではGoやNuxt、Flutterをやってます。

Comment

check コピーしました!