APNs Provider API(http2)を利用する(iOS)
Aug 25, 2016 · oldswift2apnshttp2
iOSのAPNsをAPI経由で使う方法。 しかもiOS端末からPush通知を送信する方法。
前回のNode.jsからAPNsを使う方法の派生ネタ。
APNs Provider API
はhttp2とクライアント証明書に対応さえしていればPushを送れるので、
それならiOS端末からでも良けるよね?って試してみた。
環境構築
iOSならiOS9からhttp2
に対応しているので、証明書の準備のみ必要。
証明書の準備方法は、以前のPerfect APNs編の手順でapns.p12
を書き出せばOK。
.pem
の作成やCAルート証明書は不要。
実装
ポイントはhttps
のクライアント認証を実装すること。
それができれば後はPOST形式でAPIを呼び出すだけなので簡単(APIについては前回記事参照)
クライアント認証
NSURLSession
でクライアント認証を実装するには、NSURLSessionDelegate
の
URLSession(_:didReceiveChallenge:completionHandler:)
を実装する。
サーバからクライアント認証が要求されると、このデリゲートメソッドが呼ばれるので、
クライアント証明書を読み込んでNSURLCredential
にして渡してあげればOK。
注意点は、他の認証(通常のSSL/TLS認証とかBasic認証)時も全て呼び出されるので、その実装を忘れないこと!
以上を踏まえると、実装はこんな感じ
func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
switch challenge.protectionSpace.authenticationMethod {
// 通常のhttpsのSSL/TLS認証
case NSURLAuthenticationMethodServerTrust:
// デフォルトの動作をさせる
completionHandler(.PerformDefaultHandling, nil)
// httpsのクライアント認証
case NSURLAuthenticationMethodClientCertificate:
// clientCredential(あらかじめクライアント証明書から生成した認証情報)を
// 利用して認証をかける
completionHandler(.UseCredential, clientCredential)
// その他の認証
default:
completionHandler(.PerformDefaultHandling, nil)
}
}
クライアント証明書の読み込み
クライアントの証明書はp12
形式を利用する。
証明書からはSecPKCS12Import
を使って認証情報を取り出し、NSURLCredential
を生成する。
// アプリにバンドルされているクライアント証明書(apns.p12)
guard let url = NSBundle.mainBundle().
URLForResource("apns", withExtension: "p12") else { return }
guard let p12data = NSData(contentsOfURL: p12URL) else { return }
let passphrase = "0000" // 証明書のパスフレーズ
let options = [kSecImportExportPassphrase as String : passphrase]
var items: CFArray?
guard SecPKCS12Import(p12data, options, &items) == errSecSuccess
else { return }
guard let cfarr = items else { return }
guard let certEntry = (cfarr as Array).first as? [String: AnyObject]
else { return }
let identity = certEntry["identity"] as! SecIdentity
let certificates = certEntry["chain"] as? [AnyObject]
let clientCredential = NSURLCredential(identity: identity,
certificates: certificates,
persistence: .ForSession)
今回はサンプルなのでクライアント証明書はアプリにバンドルしているが、通常はキーチェーンにいれておくべき。
なお、クライアント証明書の中身はPush送信用の一つだけが入っている前提。
小ネタなのが、certEntry["identity"] as! SecIdentity
という部分。
AnyObject
からSecIdentity
への変換は常に成功するのでas?
にはできないみたい。
詳細は公式フォーラムを参照。
APNsの送信
// デバイストークン
let deviceToken = "00fc13adff785122b4ad28809a3420982341241421348097878e577c991de8f0"
// 通知内容
let payload = "{\"aps\":{\"alert\":\"Hello!\"}}"
// 開発環境向けURL
guard let url = NSURL(string: "https://api.development.push.apple.com/3/device/")
else { return }
let request = NSMutableURLRequest(URL: url.URLByAppendingPathComponent(deviceToken))
request.HTTPMethod = "POST"
request.HTTPBody = payload.dataUsingEncoding(NSUTF8StringEncoding)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
session.dataTaskWithRequest(request).resume()
送信時のポイントは、デリゲートを指定しておくことと、completionHandler
形式のメソッドを使わないこと。
使ってしまうとデリゲートが呼び出されなくなり、クライアント認証が通らなくなる。
感想
今回は本当にネタ。多分使い道はないと思う。。。
開発環境
- OS X 10.12 Beta
- Xcode 7.3.1
- iOS 9.3.2
- iPhone 6+
ソース
クライアント証明書を上書きして使う必要があるので注意!