iOSのPush通知でAPNsとの連携を証明書と認証キーでそれぞれやってみた
iOSアプリのPush通知を実現するには、APNs(Apple Push Notification Service)と連携する必要があります。この連携方式について調べてみたので備忘録としてまとめておきます。
APNsとの連携方式
APNsとの連携はPush通知を受け取るアプリケーションとの連携と、アプリケーションにPush通知を送るようにAPNsへ依頼するアプリケーションとの連携 の2種類行う必要があります。
前者のPush通知を受け取る側の設定についてはiOSのアプリケーションのApp IDをApple Developer Console上で発行するときにPush Notificationsを有効にし、APNsの証明書と紐付けることで実現できます。証明書の発行の手順はやや複雑ですが、ググればいろんな記事がヒットします。
一方で後者のPush通知を発行する側の連携方式については、実際はAPNsと直接やりとりするケースはあまりなく、Firebase Cloud MessagingやニフティクラウドなどMBaaS製品を経由することが多いと思います。今回はこちらの連携方式について詳しく見ていきます。
Push通知をAPNsへ依頼するアプリケーションとの連携方式
Push通知をAPNsへ依頼するアプリケーションとの連携方式は、調べてみると現在は認証キー(p8ファイル)での認証方式と証明書(p12ファイル)での認証方式の2種類があるようです。
- 認証キー(p8ファイル)での認証方式
- 証明書(p12ファイル)での認証方式
認証キーでの認証方式の方が新しく、証明書での認証方式は以前からある方式のようです。それぞれのメリット・デメリットを比較すると以下のようになります。
連携方式 | メリット | デメリット |
---|---|---|
認証キー(p8ファイル)での認証 | キーの有効期限がなく更新の運用がいらない | ・Apple Developer Programに対して2つまでしか認証キーが発行できない ・App ID単位で管理ができない ・開発環境・本番環境と区別ができない |
証明書(p12ファイル)での認証 | ・App ID単位で管理ができる ・開発環境・本番環境と区別ができる |
1年単位で更新が必要となる |
メリット・デメリットはそれぞれの裏返しになっています。
認証キーの方が便利そうに見えますが、キーが2つしか発行できないので、同一アカウントで複数のアプリを開発・配信している場合は鍵の原本管理に注意が必要です。とはいえ証明書のケースでも管理は大変なので、管理対象の数が減って更新運用が不要になるのはだいぶ楽ですね。 今後はどうなっていくかについてはAppleのご都合次第ですが、恐らく認証キーの方に流れていくのではないかと予想しています。
Legacy Binary Protocolでの連携は2021年3月以降サポートされない
ちなみにもっと昔にはLegacy Binary Protocolというものもあったようです。こちらは2021年の3月以降はサポートしないとAppleから発表されています。以前のアナウンスの11月からさらに延長されました。
- Legacy Binary Protocolの連携方式
- 2021年3月以降はサポートされないという通知
- 2020年11月以降はサポートされないという通知
少し混乱しますが、ここでサポートが切れるのはLegacy Binary Protocolの連携方式であって、証明書ベースの認証方式自体は2021年3月以降も利用できると考えられます(証明書でもhttp2で通信できるますし)(でも違ったらごめんなさい...)。
各種MBaaSにおけるAPNsとの連携方式
ここで各種MBaaSのツールのAPNsとの認証方式が現在どうなっているか見てみましょう。
Firebase
Firebaseの設定手順を見ると、authentication keyとあるので認証キーでの設定が推奨されているようです。
FCMの設定画面ではAuthentication Keyが推奨と出ていますが、APNs CertificatesのアップロードUIも用意されているので今現在は両方利用できるようです。
ニフティクラウド
ニフティクラウドはp12ファイルのアップロードとあるので証明書での認証方式のようです。
AWS Amplify
AWSのBaaSツールであるAmplifyはp12ファイルのアップロードとあるので証明書での認証方式のようです。
Salesforce
セールスフォースはp12ファイルの利用をやめてp8ファイルへの以降に舵を切ったようです。
証明書での認証と認証キーでの認証は共存できる?
証明書での連携方式を今後どこかのタイミングで認証キーへ移行していくケースを考えてみます。証明書(p12ファイル)と認証キー(p8ファイル)は、いずれもPush通知の発行を依頼するサービスがAPNsへリクエストを投げる時の認証に使うものです。そのため、基本的にはAPNsの受け口が空いている限り共存できるのでは思いましたが、少し不安になったので同一アプリに対してp12ファイルで認証したPushのリクエストとp8ファイルで認証したPushのリクエストがいずれも受信できるか実際に検証してみました。
証明書一式を準備する
検証用のアプリを作るにあたっての一式をApple Developer Program上で準備します。
- 開発用証明書
- AppID(Push Notificationsをオンにする)
- provision profile(検証につかう端末を含む)
- APNs証明書(P12ファイル)
- AuthKey(p8ファイル)
iOSのサンプルプロジェクトを作る
次にPush通知を受け取るサンプルアプリを作ります。今回はPush通知が受け取れればいいので、デフォルトのSingle Page Viewアプリプロジェクトを作り、AppDelegate.swiftに最低限の設定のみを記述します。PushのCapabilitiesをオンにするのを忘れないようにします。
import UIKit import UserNotifications @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Push通知の許可ダイアログを出す UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in guard granted else { return } DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } UNUserNotificationCenter.current().delegate = self return true } ... // Push通知の用のデバイストークンをログに吐き出す func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let token = deviceToken.map { String(format: "%.2hhx", $0) }.joined() print("Device token: \(token)") } // Push通知のペイロードの受信を確認する func application(_ application: UIApplication, didReceiveRemoteNotification payload: [AnyHashable: Any]) { print(payload) } // フォワグラウンドで通知バナーを出す func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert, .sound, .badge]) } }
この状態でアプリを実機でビルドし、Push通知の許可ダイアログを許可するとログからデバイストークンが取得できます。このデバイストークンを使ってPush通知のリクエストを組みます。
証明書を使ってAPNsへPush通知のリクエストを投げる
p12ファイルを使ったAPNsへのリクエストはcurlコマンドを使って組むことができます。ググってみたらちょうどcurlコマンドがあったのでこれを参考にして組んでみます。
https://gist.github.com/greencoder/16d1f8d7b0fed5b49cf64312ce2b72cc
#!/bin/sh set -x P12_CERT_FILE_PATH="xxxxx.p12" APP_BUNDLE_INDENTIFER="com.takamiii.push-test" DEVICE_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" API_ENDPOINT="https://api.development.push.apple.com/3/device/" curl -v \ -d '{"aps":{"alert":{"title":"Test","body":"Hello from request with P12 certificates"}}}' \ -H "Content-Type: application/json" \ -H "apns-topic: ${APP_BUNDLE_INDENTIFER}" \ -H "apns-priority: 10" \ --http2 \ --cert-type P12 --cert ${P12_CERT_FILE_PATH} \ ${API_ENDPOINT}${DEVICE_TOKEN}
curlコマンドを実行すると無事200で成功し、Push通知を受信できました。ログからPushが受け取れていることがわかります。
< HTTP/2 200 < apns-id: 66AF09FA-154B-F68F-CAB3-5CBD49104122 < * Connection #0 to host api.development.push.apple.com left intact * Closing connection 0
[AnyHashable("aps"): { alert = { body = "Hello from request with P12 certificates"; title = Test; }; }]
通知バナーも出ています。
認証キーを使ってAPNsへPush通知のリクエストを投げる
p12ファイルの設定はそのままに、p8ファイルの認証キーを使ったPush通知のリクエストを投げてみます。こちらはjwtの文字列を組む必要があって少し複雑なのですが、こちらのqiita記事を参考を参考にリクエストを組んでみます。
#!/bin/bash set -x AUTH_KEY_FILE_PATH="AuthKey_xxxxxx.p8" AUTH_KEY_ID="xxxxxxxx" TEAM_ID="xxxxxxxxx" APP_BUNDLE_INDENTIFER="com.takamiii.push-test" DEVICE_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" API_ENDPOINT="https://api.development.push.apple.com/3/device/" base64() { openssl base64 -e -A | tr -- '+/' '-_' | tr -d = } sign() { printf "$1"| openssl dgst -binary -sha256 -sign "${AUTH_KEY_FILE_PATH}" | base64 } TIME=$(date +%s) HEADER=$(printf '{ "alg": "ES256", "kid": "%s" }' "${AUTH_KEY_ID}" | base64) CLAIMS=$(printf '{ "iss": "%s", "iat": %d }' "${TEAM_ID}" "${TIME}" | base64) JWT="${HEADER}.${CLAIMS}.$(sign ${HEADER}.${CLAIMS})" curl -v \ -d '{"aps":{"alert":{"title":"Test","body":"Hello from request with P8 certificates"}}}' \ -H "Content-Type: application/json" \ -H "Authorization: Bearer ${JWT}" \ -H "apns-topic: ${APP_BUNDLE_INDENTIFER}" \ --http2 \ ${API_ENDPOINT}${DEVICE_TOKEN}
こちらも無事200で成功し、Push通知を受信できました。ログからPushが受け取れていることがわかります。
< HTTP/2 200 < apns-id: 09393B36-E1CE-4F9A-5A29-2FC7FF73D424 < * Connection #0 to host api.development.push.apple.com left intact * Closing connection 0
[AnyHashable("aps"): { alert = { body = "Hello from request with P8 certificates"; title = Test; }; }]
通知バナーも出ています。
以上から、p12ファイルとp8ファイルを使ったリクエストは併用できているので、証明書と認証キーの設定は共存できていると考えられます。
なのでもともとニフティクラウドでp12ファイルでPushを送っていたところからp8ファイルを使ったFirebaseでのPushに移行する、なんてことは問題なくできそうということですね。
まとめ
iOSのPush通知についてAPNsとの連携方式ついて調べた結果をまとめてみました。Push通知の実現にはアプリの実装に加えて証明書連携が必要で、その手順は複雑ですが一つ一つ紐解いていけば理解できるものだと思いました。
またAPNsの証明書と認証キーの移行については今のところどちらでも利用できているので、Pushを実際に発行するアプリケーションの連携方式に合わせて利用していけばよいでしょう。認証キーでの連携は今後は増えていくと思われるので、鍵ファイルの管理をきちんとしていく必要があると感じました。
おまけ
Push通知のリクエストのエンドポイントを調査している中でapi.sandbox.push.apple.com
とapi.development.push.apple.com
の2つがあって混乱したので、digで掘ってみました。
$ dig api.sandbox.push.apple.com ; <<>> DiG 9.10.6 <<>> api.sandbox.push.apple.com ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32012 ;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 512 ;; QUESTION SECTION: ;api.sandbox.push.apple.com. IN A ;; ANSWER SECTION: api.sandbox.push.apple.com. 269 IN CNAME api.sandbox.push-apple.com.akadns.net. api.sandbox.push-apple.com.akadns.net. 185 IN A 17.188.138.73 ;; Query time: 14 msec ;; SERVER: 2404:1a8:7f01:b::3#53(2404:1a8:7f01:b::3) ;; WHEN: Mon Jul 13 18:40:18 JST 2020 ;; MSG SIZE rcvd: 122
$ dig api.development.push.apple.com ; <<>> DiG 9.10.6 <<>> api.development.push.apple.com ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 45266 ;; flags: qr rd ra; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 512 ;; QUESTION SECTION: ;api.development.push.apple.com. IN A ;; ANSWER SECTION: api.development.push.apple.com. 242 IN CNAME api.sandbox.push-apple.com.akadns.net. api.sandbox.push-apple.com.akadns.net. 554 IN A 17.188.165.218 api.sandbox.push-apple.com.akadns.net. 554 IN A 17.188.166.29 api.sandbox.push-apple.com.akadns.net. 554 IN A 17.188.165.219 api.sandbox.push-apple.com.akadns.net. 554 IN A 17.188.166.27 ;; Query time: 17 msec ;; SERVER: 2404:1a8:7f01:b::3#53(2404:1a8:7f01:b::3) ;; WHEN: Mon Jul 13 18:40:36 JST 2020 ;; MSG SIZE rcvd: 174
どっちも最終的には同じapi.sandbox.push-apple.com.akadns.net
を向いてるようなので、どちらでもよさそうです。