Golangで「プロバイダー認証トークン」を生成して、APNsにプッシュを送ろう

by

cat9V9A9026_TP_V

サーバー開発担当の木下です。
Google I/O 2017が開催されましたね。Androidがkotlinを正式にサポートすることになったというニュースに、社内も若干の盛り上がりを見せています。

さてkotlinの話を振っておいて何ですが、今回はiOSのプッシュ(APNs)とGolangの話です。

APNsの大きな仕様変更

WWDC 2015で、APNs Provider APIが発表されました。これは、従来のAPNsがソケット通信でプッシュのリクエストを送っていたのに代わって、HTTP/2でリクエストできるようになるというものです。1端末=1リクエストになりますが、ソケット通信時よりも細かなレスポンスが得られるため、不着の場合の原因の切り分けや、使えなくなったトークンの処理などが容易になりました。
さらにWWDC 2016で、「プロバイダー認証トークン」が使えるようになるという発表もありました。従来のプロバイダー証明書は1年の有効期限付きだったため、毎年Apple Developerで証明書を発行してサーバーでpemファイルを生成するという作業が必要でした。これに対して、プロバイダー認証トークンは、有効期限のない証明書で署名したトークンのため、毎年証明書を更新する必要がないというメリットがあります。

「プロバイダー認証トークン」について

公式のドキュメントに、説明が記載されています:
APNs Provider API – Apple Developer

が、若干わかりにくいため、説明を加えたいと思います。
JWTはJSON Web Tokenの略で、JSONをURL Safeになるようbase64にエンコードしたものです。さらにこれを、証明書を用いて署名したものをJWS(JSON Web Signature)、暗号化したものをJWE(JSON Web Encryption)と言います。このうち、APNs Provider APIに送るのはJWSです。上記ドキュメントの中に「トークンの暗号化には〜」という文言があるため、てっきりJWEかと思ってしまいますが、JWEを送るとエラーが返ってきます。

ヘッダーペイロードに含める’alg’は’ES256’で固定、クレームペイロードに含める’iat’はトークンを生成した時刻をUnixtimeで入れます。残りの’kid’と’iss’は、後ほど説明します。

.p8証明書を取得する

これまでAPNsで使用する証明書といえば「Apple Push Notification Authentication Key」で、ダウンロードすると「.p12」という拡張子の証明書が発行されました。これををもとに「.pem」ファイルを生成するという作業が必要でした。
今回のプロバイダー認証トークンの生成に必要なのは上記証明書ではなく、「Apple Push Notification Service SSL」というものです。ダウンロードすると「.p8」という拡張子のものが発行されます。このとき発行される証明書のファイル名は「APNsAuthKey_ABCDE12345.p8」という形になるはずです。このファイル名に含まれる「ABCDE12345」にあたる部分(10桁の英数字)が、前述した’kid’になります。トークンを生成する際に必要なので、メモしておきましょう。

もうひとつのパラメーター’iss’ですが、Apple Developer内の「Membership」ページにある「Team ID」がそのままissの値になります。
screenshot

トークンを生成する

いよいよトークンを生成してみましょう。今回はGolangを使ってみることにしました。Golangを選定した理由は、

  • JWT生成に使うライブラリーが良さげだった
  • トークンは1時間使える→1時間に1回生成すれば良い→Crontabで回すならPHPじゃなくてもいいんじゃない?
  • cURLのバージョンに左右されるPHPと違い、Golang標準のhttp.ClientがHTTP/2をサポートしているので、APNs Provider APIとの親和性が高い
  • Golangを触ってみたかった

などなど。4つ目重要です。

まず下準備として、ローカルにGoの環境がまだなければ、インストールしておきましょう。(http://golang-jp.org/)
またgithub.com/dvsekhvalnov/jose2goも必要ですので、go getするなどしてインポートしておいてください。

プロジェクトを新規作成し、main.goとか適当な名前をつけて↓を書き込みましょう。

package main

import (
	"os"
	"fmt"
	"time"
	"io/ioutil"
	"encoding/json"
	"github.com/dvsekhvalnov/jose2go/keys/ecc"
	"github.com/dvsekhvalnov/jose2go"
)

type Payload struct {
	Iss string `json:"iss"`
	Iat int64  `json:"iat"`
}

func main(){

	var keyFilePath string = os.Args[1]
	var teamId string = os.Args[2]
	var keyId string = os.Args[3]

	var now int64 = time.Now().Unix()

	var payload = Payload {Iss: teamId, Iat: now}

	jsonBytes, e := json.Marshal(payload)

	if (e != nil) {
		panic("failed marshaling json")
	}

	keyBytes, e := ioutil.ReadFile(keyFilePath)

	if (e != nil) {
		panic("invalid key file")
	}

	privateKey, e := ecc.ReadPrivate(keyBytes)

	if (e != nil) {
		panic("invalid key format")
	}

	signedToken, e := jose.Sign(string(jsonBytes), jose.ES256, privateKey,
		jose.Header("alg", "ES256"),
		jose.Header("kid", keyId))

	if (e != nil) {
		panic("failed signing")
	} else {
		fmt.Println(signedToken)
	}

}

保存できたら、ビルドしましょう。
$ GOOS=linux GOARCH=amd64 go build main.go
おおよそ3MBほどの実行ファイルが出力されると思います。

main.goにあるfunc main()内のos.Argsはスライスで、0番目にプログラムのパス、1番目移行にコマンドで渡した引数が入ります。
実際に実行してみましょう。
$ ./main ./APNsAuthKey_ABCDE12345.p8 TEAMTEAMID ABCDE12345
標準出力に長い英数字が出てくるはずです。これが生成されたJWTになります。
1時間有効なので、生成した時刻と一緒にKVSなどに保存しておきましょう。

APNs Provider APIに送信する

本当は送信もGolangで済ませてしまいたいところですが、メンテナンスのしやすさから今回はPHPにしました。

<? php
    $http_header = array(
        "apns-topic: {$apns_topic}", // <- アプリのbundle ID
        "authorization: Bearer {$token}" // <- さっき生成したJWT
    );
    $url = "https://api.push.apple.com/3/device/{$deviceToken}"; // <- 端末のプッシュトークン

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $http_header);

    $data = json_encode(array(
        'aps' => array(
            'alert' => array(
                'title'  => (string)'新着メッセージ',
                'body'  => (string)'こんにちは'
            ),
            'sound' => (string)'default'
        )
    ));

    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);

    $response = curl_exec($ch);
    $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    curl_close($ch);

push.phpとか適当に名前をつけて保存し、実行してみましょう。
$ php -f ./push.php

ss3
無事に送信できました!

iOSへのプッシュが格段に便利になりました

これまでのソケット通信によるリクエストに比べて、レスポンスの安定感や更新不要の証明書など、格段に便利になった感があります。また、PHPで書くと、FCM(Android)へのプッシュと同じようなソースになるため、メンテナンス・改修も楽になったと思います。
yumなどのパッケージマネージャーでインストールできるcURL等がHTTP/2をサポートするようになれば、更に普及が進むのではないでしょうか。

またイーディーエーには、スマホアプリへのプッシュ通知に関する豊富なノウハウがあります。ぜひお気軽にお問い合わせください!

木下 晴晶
e.d.aではサーバー・APIの開発を担当。Plamo->Fedora->Ubuntu->CentOSとわたり歩く生粋のLinux好き。趣味はゆったりした鉄道旅行。

Egg Device Application

東京品川のスマホアプリ開発会社です。
一般アプリ、業務用アプリからVRまで開発可能。

ライター一覧

求人情報

スマホアプリ開発の
相談を受け付けています

メールでのご相談

お電話でのご相談
TEL 03-5422-7524
平日10:00~18:00