アプリ開発日誌
2017.05.26
Golangで「プロバイダー認証トークン」を生成して、APNsにプッシュを送ろう
サーバー開発担当の木下です。
Google I/O 2017が開催されましたね。Androidがkotlinを正式にサポートすることになったというニュースに、社内も若干の盛り上がりを見せています。
さてkotlinの話を振っておいて何ですが、今回はiOSのプッシュ(APNs)とGolangの話です。
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’は、後ほど説明します。
これまでAPNsで使用する証明書といえば「Apple Push Notification Service SSL」で、ダウンロードすると「.p12」という拡張子の証明書が発行されました。これををもとに「.pem」ファイルを生成するという作業が必要でした。
今回のプロバイダー認証トークンの生成に必要なのは上記証明書ではなく、「Apple Push Notification Authentication Key」というものです。ダウンロードすると「.p8」という拡張子のものが発行されます。このとき発行される証明書のファイル名は「APNsAuthKey_ABCDE12345.p8」という形になるはずです。このファイル名に含まれる「ABCDE12345」にあたる部分(10桁の英数字)が、前述した’kid’になります。トークンを生成する際に必要なので、メモしておきましょう。
もうひとつのパラメーター’iss’ですが、Apple Developer内の「Membership」ページにある「Team ID」がそのままissの値になります。

いよいよトークンを生成してみましょう。今回は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などに保存しておきましょう。
本当は送信も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

無事に送信できました!
これまでのソケット通信によるリクエストに比べて、レスポンスの安定感や更新不要の証明書など、格段に便利になった感があります。また、PHPで書くと、FCM(Android)へのプッシュと同じようなソースになるため、メンテナンス・改修も楽になったと思います。
yumなどのパッケージマネージャーでインストールできるcURL等がHTTP/2をサポートするようになれば、更に普及が進むのではないでしょうか。
またイーディーエーには、スマホアプリへのプッシュ通知に関する豊富なノウハウがあります。ぜひお気軽にお問い合わせください!