新的APNs协议基于HTTP/2,一种是使用Universal Push Notification Client SSL 证书,一种是使用Token,本文主要讲基于token的APNs协议。
基于HTTP/2与Token的 APNs 协议
APNs Provider(即,APP的后台) API 允许您向您的 iOS,macOS 设备上的应用程序和 Apple Watch 发送远程通知。API 基于 HTTP/2 网络协议。每个交互通过一个 POST 请求,包含 JSON 的有效Payload负载,通过服务器使用Auth Key生成服务端token连接APNs服务器,并且通过设备token发送负载。APNs然后转发给特定设备的指定应用程序。
- Request 和 Response 使用JSON通信
- APNs支持状态码和返回 error 信息
- APNs推送成功时 Response 将返回状态码200
- APNs推送失败时,Response 将返回 JSON 格式的 Error 信息。
- 最大推送长度提升到4096字节(4Kb)
- 可以通过 “HTTP/2 PING ” 心跳包功能检测当前 APNs 连接是否可用,并能维持当前长连接。
- 支持为不同的APP定义 “topic”(其实就是App Bundle ID)
- 多个推送App,只需要一个Apple Push APNs Auth Key
Apple Push Notification Authentication Key
2016年9月,苹果悄悄上线了token验证的推送方式,通过获得一个认证密钥(APNs Auth Key)去生成服务器端token,并且token非常容易生成,可以使用这些token令牌代替推送证书。一个认证密钥可用于多个应用程序并且永远不过期。每一个需要推送的App都需要配置推送证书的时代过去了。but,大部分第三方推送服务商,目前都没有升级到APNs Auth Key Token模式。
Auth key 生成
开发者网站证书页面:https://developer.apple.com/account/ios/certificate/key
选择Apple Push Notification Authentication Key (Sandbox & Production)并且点击页面底部的Continue
网站会生成包含APNs Auth Key的.p8密钥文件,下载下来APNsAuthKey_xxxxxxx.p8后续连接APNS使用。
协议说明
这种方式适合在基于HTTP/2协议的Provider使用,它与APNs之间的连接通过JWT(JSON web tokens)来验证。在这种方式下不需要使用证书+私钥的方式来建立可靠连接。Provider只需要提供一对公私钥(私钥给APNs保存,公钥Provider自己保存),并使用其中的私钥生成并加密JWT Token,每次向APNs请求推送的时候带上这个Token即可。token必须定期更新,每一个APNs provider验证token有效期为一个小时。
具体步骤如下:
- Provider通过
TLS
向APNs发起请求。 - APNs返回一个证书给Provider。
- Provier验证这个证书。通过后,发送push数据并带上JWT token。
- APNs验证token,并返回请求的结果。
重要:
建立TLS连接必须要有一个
GeoTrust Global CA root certificate
,在macOS中,这个证书已经安装在keychain中,如果是其他操作系统则可以在GeoTrust Root Certificates website下载。
HTTP/2请求APNs
字段名 | 字段值 |
---|---|
:method | POST |
:path | /3/device/<device-token> |
对于 <device-token> 参数,指定目标设备的十六进制字节。
APNs需要使用 HPACK (HTTP/2 的报头压缩),防止重复的标题键和值。APNs为HPACK维护一个小的动态表。为了避免填满了APNs HPACK表,因而必须丢弃表数据,编码headers以下列方式 — — 尤其是当发送大量的流︰
:method 必须为POST
:path 值应编码为一个header中的一个文本字段,没有索引
authorization 请求header,如果存在,应编码为一个header中的一个文本字段,没有索引
apns-id、 apns-expiration,和 apns-collapse-id 不同的编码取决于它是否是第一次初始请求或随后的 POST 操作的一部分,如下︰
第一次发送请求头的时,使增量索引编码允许头名称加入动态表中
子请求发送头时,编码成header中的一个文本字段,没有索引
请求头
Header 头 | 描述 |
---|---|
authorization | 提供的发送到指定(app)主题通知的APNs验证token,token是以Base64 URL编码的JWT格式。指定为bearer <provider token>
当使用证书连接的时候,这个请求头将会被忽略 |
apns-id | 一个规范的的 UUID 用来标识通知。如果发送通知时发生错误,APNs 使用此值来标识通知,通知到您的服务器。
规范的格式是 32 个小写的十六进制数字,由连字符分隔为5,8-4-4-4-12。UUID一个例子如下:
如果您省略这个头,一个新的UUID由APNs创建并且在response中返回 |
apns-expiration | 通知过期时间,秒级的UTC时间戳,这个header标识通知 从何时起不再有效,可以丢弃。
如果这个值非零,APNs保存通知并且尽量至少送达一次。如果无法第一时间送达,根据需要重复尝试。 如果这个值为0,APNs认为通知立即过期,不会存储与重新推送。 |
apns-priority | 通知的优先级,指定以下值:
如果省略这个头,APNs服务器会把优先级设置为10 |
apns-topic | 远程通知的主题,通常是你App的bundle id,在开发者账号中创建的证书必须包含此bundle id
如果证书包含多个主题,这个头必须指定一个值 如果省略此头并且APNs证书不包含指定的主题,APNs服务器使用证书的Subject作为默认主题。 如果使用token代替证书,必须指定此头,提供的主题应该被在开发者账号中的team提供。即bundle id的app应该与auth key同属于一个开发者组。 |
apns-collapse-id | 具有相同的折叠标识符的多个通知推送给用户合并显示为单个通知。比如: apns-collapse-id : 2 ,那么value为2的消息将被APNS合并成一条消息推送给设备。此关键字的值不能超过 64 个字节。更多的信息,请参阅Quality of Service, Store-and-Forward, and Coalesced Notifications。 |
Provider Authentication Tokens
关于JWT(JSON Web Token)的详细资料可以通过这里了解。同时也可以从这里找到一些现成可用的库。
下面对JWT进行详细的介绍,一个JWT实际上是一个JSON对象,它的头部必须包含:
- 用以加密token的加密算法(alg) ,比如:ES256。
- 10个字符长度的标识符(kid),(苹果开发者网站创建的APNs Auth Key详情中的key id)
同时他的claims payload部分必须包含:
- issuer(iss) registered claim key,其值就是10个字符长的Team ID。
- issued at (iat) registered claim key,其值是一个秒级的UTC时间戳。
比如:
1 2 3 4 5 6 7 8 |
{ "alg": "ES256", "kid": "ABC123DEFG" } { "iss": "DEF123GHIJ", "iat": 1437179036 } |
创建完这个token后,必须使用自己的私钥对其进行加密,然后再采用基于P-256曲线和SHA-256哈希算法的椭圆曲线数字签名算法(ECDSA)进行签名,并将alg
键的值设置为ES256
。(注意:APNs只支持ES256签名的JWT,否则会返回InvalidProviderToken(403)
错误)
为了保证安全,APNs要求定期更新token,时间间隔为1小时,如果APNs发现当前的时间戳与iat
值中的时间戳相比,大于一个小时,那么APNs会拒绝推送消息,并返回ExpiredProviderToken (403)
错误。
请求体
1 |
{ "aps" : { "alert" : "Hello HTTP/2" } } |
响应头
Header 名 | Header值 |
---|---|
apns-id | apns-id 从request得到, 如果request中没有此值, APNs服务器创建一个新的UUID并且在header中返回 |
:status | HTTP状态码,HTTP status code.可能status codes, 参加下文 |
响应状态码
Status code | Description |
---|---|
200 | 成功 |
400 | 无效请求 |
403 | 证书错误或者验证token错误 |
405 | :method 设置错误. 只支持 POST 请求 |
410 | device token不在有效与topic |
413 | 负载payload太大 |
429 | 服务端对于同一个device token发送了太多了请求 |
500 | 内部服务器错误 |
503 | 服务器关闭,不可用 |
使用
Sample request for a provider authentication token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
HEADERS - END_STREAM + END_HEADERS :method = POST :scheme = https :path = /3/device/00fc13adff785122b4ad28809a3420982341241421348097878e577c991de8f0 host = api.development.push.apple.com authorization = bearer eyAia2lkIjogIjhZTDNHM1JSWDciIH0.eyAiaXNzIjogIkM4Nk5WOUpYM0QiLCAiaWF0I jogIjE0NTkxNDM1ODA2NTAiIH0.MEYCIQDzqyahmH1rz1s-LFNkylXEa2lZ_aOCX4daxxTZkVEGzwIhALvkClnx5m5eAT6 Lxw7LZtEQcH6JENhJTMArwLf3sXwi apns-id = eabeae54-14a8-11e5-b60b-1697f925ec7b apns-expiration = 0 apns-priority = 10 apns-topic = <MyAppTopic> DATA + END_STREAM { "aps" : { "alert" : "Hello" } } |
APNs Auth Key 与 shell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
#!/bin/bash # Get curl with HTTP/2 and openssl with ECDSA: 'brew install curl openssl' curl=/usr/local/opt/curl/bin/curl openssl=/usr/local/opt/openssl/bin/openssl # -------------------------------------------------------------------------- # HostDevelopment = "https://api.development.push.apple.com" # HostProduction = "https://api.push.apple.com" deviceToken=b27371497b85611baf9052b4ccfb9641ab7fea1d01c91732149c99cc3ed9342f authKey="./APNSAuthKey_ABC1234DEF.p8" authKeyId=ABC1234DEF teamId=TEAM123456 bundleId=com.example.myapp endpoint=https://api.development.push.apple.com read -r -d '' payload <<-'EOF' { "aps": { "badge": 2, "category": "mycategory", "alert": { "title": "my title", "subtitle": "my subtitle", "body": "my body text message" } }, "custom": { "mykey": "myvalue" } } EOF # -------------------------------------------------------------------------- base64() { $openssl base64 -e -A | tr -- '+/' '-_' | tr -d = } sign() { printf "$1"| $openssl dgst -binary -sha256 -sign "$authKey" | base64 } time=$(date +%s) header=$(printf '{ "alg": "ES256", "kid": "%s" }' "$authKeyId" | base64) claims=$(printf '{ "iss": "%s", "iat": %d }' "$teamId" "$time" | base64) jwt="$header.$claims.$(sign $header.$claims)" $curl --verbose \ --header "content-type: application/json" \ --header "authorization: bearer $jwt" \ --header "apns-topic: $bundleId" \ --data "$payload" \ $endpoint/3/device/$deviceToken |
后续更新其他语言推送方法。
参考资料
http://thrysoee.dk/apns/
转载请注明:天狐博客 » 基于HTTP/2与Token的APNs新协议