スマートデバイスアプリ開発のあれやこれや

Token認証対応のAPNs ProviderをJavaで実装する

 久しぶりのiOSネタです。iOSバイス相手のPUSH通知を実現するApple Push Notification Services(APNs)ですが、JWT認証の方を使ったことがなかったので今回はそちらに挑戦してみようと思います。

おさらい

 APNsサーバは、Providerとの交信方法を2種類提供しています。

  • バイナリProvider API
  • APNs Provider API

 「バイナリ Provider API」は、iOS黎明期からサポートされてきた昔ながらの交信方法です。APNsサーバとProviderサーバ間でソケット通信を行い、所定フォーマットのバイナリデータで通知を依頼します。自前で実装する場合、ペイロード(依頼内容)のバイナリ変換が面倒くさい印象があります。AppleでもこれからProviderサーバを実装するのであれば、バイナリProvider APIは非推奨……と言っています。その内、サポート切られるのでは?

 「APNs Provider API」は、WWDC2015で発表された新しい交信方法です。HTTPのGET/POSTメソッドでAPNsサーバに対して通知依頼を出すことが出来るので、非常に簡便にプッシュ通知を実現できるようになりました。また、通知失敗時もHTTPリクエストのレスポンスで詳細な原因が返却されますので、再通知やデバッグが捗る点も魅力的ですね。

 交信方法やエラーハンドリングの簡便さ以外に「APNs Provider API」の大きな特徴が、APNsサーバとの認証方式です。

APNsサーバとの認証方式

 「APNs Provider API」は以下の2種類の認証方式をサポートしています。

  • 証明書認証
  • JWT認証

 「証明書認証」は、Member Centerから発行したAPNs用SSL証明書を使った認証です。証明書の有効期限が1年で失効されるため、定期的に証明書を再生成して差し替えないと通知が送れなくなるというデメリットがあります。また、証明書はアプリ単位に作成するので管理も煩雑になりがちです。受託開発案件をやっている人は、結構面倒くさいのではないでしょうか?保守料を取れるという見方もありますが。ちなみに、バイナリProvider APIは、証明書認証だけしかサポートしていません。

 「JWT認証」は、Member Centerから発行したAPNs Auth Keyを使った認証です。APNs Auth Key自身には有効期限が存在せず、このAPNs Auth Keyを使ってAPNsサーバに認証トークンを要求する点が大きな特徴であり、これらの認証トークン取得手続きは全てProviderサーバのプログラムで行う必要があります。その代わり、1度処理を実装してしまえばAPNs Auth Keyを手動で失効させない限り、ずっとプッシュ通知を送り続けられる環境が実現できます。証明書認証と異なり、通知先のアプリに依存する要素が少ない点も嬉しいです。

JavaでProviderサーバ(JWT認証)を実装する

 本題です。フル自前実装でも良いのですが、OSSを活用しましょう。

github.com

(1) pom.xmlの編集

 Mavenに登録されているので、Providerサーバ用プロジェクトのpom.xmlに依存関係を追記します。

<dependency>
    <groupId>com.relayrides</groupId>
    <artifactId>pushy</artifactId>
    <version>0.9.3</version>
</dependency>

 あと、ポイントとしてはログ出力用のライブラリも追加した方が無難です。通知に失敗したときの詳細挙動がログから追えるからです。Java標準のエラー出力だと原因調査が難しいので、ぜひ設定しましょう。

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

(2) 通知処理の実装

 あとは簡単です。

try {
        // 通知クライアントを作成
    final ApnsClient apnsClient = new ApnsClientBuilder().build();
    apnsClient.registerSigningKey(new File("[APNs Auth Keyが置かれた場所までのファイルパス]"), 
                                "[TeamId]", 
                                "[APNs Auth Keyの認証コード]", 
                                "[通知先アプリのAppID]");
        // ペイロードを作成
    ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder();
    payloadBuilder.setAlertBody("hoge");
    String payload = payloadBuilder.buildWithDefaultMaximumLength();
        // 開発用APNsサーバに接続
    Future<Void> connectFuture = apnsClient.connect(ApnsClient.DEVELOPMENT_APNS_HOST);
        // 接続完了まで同期で待つ
    connectFuture.get();
        // 通知オブジェクトを作成
    SimpleApnsPushNotification pn = new SimpleApnsPushNotification("[デバイスToken]", "[通知先アプリのAppID]", payload);
        // 通知実行
    apnsClient.sendNotification(pn);
        // APNsサーバから切断
    apnsClient.disconnect();
} catch (SSLException e) {
    e.printStackTrace();
} catch (InvalidKeyException e) {
    e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    e.printStackTrace();
}