スタッフブログ

STAFF BLOG

アプリ開発日誌

2017.05.26

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

サーバー開発担当の木下です。
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 Service SSL」で、ダウンロードすると「.p12」という拡張子の証明書が発行されました。これををもとに「.pem」ファイルを生成するという作業が必要でした。
今回のプロバイダー認証トークンの生成に必要なのは上記証明書ではなく、「Apple Push Notification Authentication Key」というものです。ダウンロードすると「.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をサポートするようになれば、更に普及が進むのではないでしょうか。

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

BACK

お問合せ

イーディーエーに興味をお持ちいただいて
ありがとうございます!
スマホアプリに関するご相談、
お見積りや弊社へのご質問など、
お気軽にお問い合わせください。
担当者より折り返しご連絡させていただきます。

    お名前必須
    会社名
    メールアドレス必須
    電話番号必須
    お問合わせ種別必須
    お問合わせ内容必須

    アンケートにご協力ください。
    弊社サイトへはどのようにしてアクセスされましたか?

    個人情報のお取扱いに関する同意事項

    1.事業者の氏名又は名称

    株式会社イーディーエー

    2.個人情報保護管理者の氏名又は職名、所属及び連絡先

    個人情報保護管理者 小宮 保人
    Mail:[email protected]

    3.取得した個人情報の利用目的

    当フォームで取得した個人情報は、お問い合わせに関する回答のために利用し、目的外利用はいたしません。

    4.弊社が取得した個人情報の第三者への委託、提供について

    弊社は、ご本人に関する情報をご本人の同意なしに第三者に委託または提供することはありません。

    5.個人情報保護のための安全管理

    弊社は、ご本人の個人情報を保護するための規程類を定め、従業者全員に周知・徹底と啓発・教育を図るとともに、その遵守状況の監査を定期的に実施いたします。
    また、ご本人の個人情報を保護するために必要な安全管理措置の維持・向上に努めてまいります。

    6.個人情報の開示・訂正・利用停止等の手続

    ご本人が、弊社が保有するご自身の個人情報の、利用目的の通知、開示、内容の訂正、追加又は削除、利用の停止、消去及び第三者への提供の停止を求める場合には、下記に連絡を頂くことで、対応致します。

    株式会社イーディーエー 個人情報お問合せ窓口
    〒106-0032 東京都港区六本木7丁目14番23 ラウンドクロス六本木4F
    TEL:03-5422-7524 FAX:03-5422-7534
    Mail:[email protected]

    7.ご提供いただく情報の任意性

    個人情報のご提供は任意ですが、同意を頂けない場合には、第3項にあります利用目的が達成できない事をご了承いただくこととなります。

    8.弊社Webサイトの運営について

    弊社サイトでは、ご本人が弊社Webサイトを再度訪問されたときなどに、より便利に閲覧して頂けるよう「クッキー(Cookie)」という技術を使用することがあります。これは、ご本人のコンピュータが弊社Webサイトのどのページに訪れたかを記録しますが、ご本人が弊社Webサイトにおいてご自身の個人情報を入力されない限りご本人ご自身を特定、識別することはできません。
    クッキーの使用を希望されない場合は、ご本人のブラウザの設定を変更することにより、クッキーの使用を拒否することができます。その場合、一部または全部のサービスがご利用できなくなることがあります。