引言
最近在设计后台服务的时候引入了JWT,正好想到在iOS10中APNs服务添加了和JWT有关的更新,但还没有来得及实践,于是借此机会码一把。
JSON Web Token (JWT)
JWT是基于JSON、开放的工业标准RFC 7519,用于通信双方安全的互相声明。比如用户登录后,服务器可以生成一个jwt token,保存后返回给用户。该token中指明了用户id和用户角色,当用户需要请求某些操作或资源时,头部添加该jwt token,服务器可以快速的鉴别该请求是否有效合法。因为只是包含了用户id和角色这些非隐私的数据,因此不用担心泄露数据或其他风险。
从结构上看,jwt token包含3部分:
- header: dictionary,包含签名算法alg和token类型typ
- payload: dictionary,有效载荷,包含一些数据
- signature: 对header和payload的签名,规则
sign(base64UrlEncode(header) + "." + base64UrlEncode(payload), private_key)
payload中包含若干声明claims,有3种类型:
- Registered Claim: 可以理解为预置的,有iss/sub/aud/iat等
- Public Claim: 为了防止名字冲突,使用”JSON Web Token Claims”确立的或是公有的
- Private Claim Names: 私有的,自定义的;上面两种声明之外的
完整的jwt token为:base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + signature
。
当客户端发送请求的时候,需要在头部headers添加token:
Authorization: Bearer jwt_token
另外token可以添加过期时间等限制,避免token泄漏后被滥用。
推荐一个网站,可以encode/decode token:jwt debugger。同时,该网站也收集了各种语言的jwt实现,在下面我们会再提及。
配置
在开始之前,需要设置相关Apple ID的推送属性,这一步可以在开发者网站做,也可以在Xcode中操作。接着,配置推送证书,这一步只能在开发者网站上操作。因为我们是尝试使用jwt token的通信方式,那么我们需要生成一个key:在创建证书的功能模块下,找到 keys
,进行添加。生成的文件是你的私钥,需要妥善保管;key ID需要记下,在后面需要使用。这些完成后,就可以进行客户端和Provider的开发了。
客户端准备
iOS客户端需要做的事情简要如下:
- 注册推送服务
- 实现
UNUserNotificationCenterDelegate
:当注册成功后把token发送到Provider;否则提示用户 - 注册成功后,添加推送的消息分类
- 实现
UNUserNotificationCenterDelegate
:收到消息后,App的响应
其中,token相当于设备的标识符,当Provider需要推送服务给某个设备的时候,会把该token发送到APNs。
同时要注意,在App每次启动的时候,可以检查用户是否同意了消息提示。如果没有同意的话,需要适时的再次注册推送服务。
做为示例,这里创建AppDelegate的extension,把相关操作聚合在一起:
//APNS
extension AppDelegate:UNUserNotificationCenterDelegate {
func registerRemoteNotification() {
UIApplication.shared.registerForRemoteNotifications()
}
func requestAuth() {
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.badge,.alert]) { (granted, error) in
if false == granted {
//do sth
}
}
}
func registerNotificationCategory() {
let center = UNUserNotificationCenter.current()
let category = UNNotificationCategory(identifier: "general", actions: [], intentIdentifiers: [], options: .customDismissAction)
// Create the custom actions for the TIMER_EXPIRED category.
let snoozeAction = UNNotificationAction(identifier: "SNOOZE_ACTION",
title: "Snooze",
options: UNNotificationActionOptions(rawValue: 0))
let stopAction = UNNotificationAction(identifier: "STOP_ACTION",
title: "Stop",
options: .foreground)
let expiredCategory = UNNotificationCategory(identifier: "TIMER_EXPIRED",
actions: [snoozeAction, stopAction],
intentIdentifiers: [],
options: UNNotificationCategoryOptions(rawValue: 0))
center.setNotificationCategories([category, expiredCategory])
}
//MARK: - UNUserNotificationCenterDelegate
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
debugPrint("\(#function): \(#line)")
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
debugPrint("\(#function): \(#line)")
}
//MARK: - Appdelegate
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
sendToken(deviceToken)
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
debugPrint(error)
}
}
关于推送的客户端部分,因为相关的资料很多,这里就不赘述了。
使用token和APNs通信
这里我们使用Express进行Provider的开发,具体的开发环境: Node.js v8.x.x(LTS),Express 4.x.x。使用Express Generator 创建工程,然后在 index.js文件中添加接收客户端发送的token:
router.post('/sendToken', function (req, res, next) {
console.log(req.body);
var token = null, appName = null;
if (req.body.token) token = req.body.token;
if (req.body.appname) appName = req.body.appname;
var result = {
'retCode': 200,
'msg': 'OK'
};
res.status(200).json(result);
});
这里为了简单,没有做authentication,POST参数校验,token的存储等工作。
接下来,我们在网页上添加一个按钮,当点击按钮后,Provider向APNs发送推送消息。
//index.jade
a(href='/apns') APNS Test
接着继续在index.js中添加方法:
const jwt = require('jsonwebtoken');
const fs = require('fs');
const http2 = require('http2');
router.get('/apns', function (req, res, next) {
var cert = fs.readFileSync('./routes/auth.p8');
var jwtToken = jwt.sign({ iss: 'YOUR_DEVELOPER_ID' }, cert, { algorithm: 'ES256', header: { 'kid': 'YOUR_KEY_ID' } }); //1
console.log(jwtToken);
// http2 client
const client = http2.connect('https://api.development.push.apple.com:443');;
client.on('error', (err) => {
console.error(err);
});
const playload = {'aps': { 'badge': 2, 'alert': 'this is a test' }};//2
var payloadData = JSON.stringify(playload);
const apnReq = client.request({
':path': '/3/device/YOUR_RECEIVED_TOKEN',
':method': 'POST',
'authorization': 'bearer ' + jwtToken,
'apns-topic': 'YOUR_BUNDLE_ID'
});//3
apnReq.on('response', (headers, flags) => {
console.log('http2 response:');
for (const name in headers) {
console.log(`${name}: ${headers[name]}`);
}
});//4
apnReq.setEncoding('utf8');
let data = '';
apnReq.on('data', (chunk) => { data += chunk; });
apnReq.on('end', () => {
console.log('end:');
console.log(`${data}`);
client.destroy();
});
apnReq.write(payloadData); //5
apnReq.end();
res.render('index', { title: 'Express' });
});
使用 npm start
后启动过程,控制台也许会提示 http/2 是 experimental API。不用担心,这不会影响项目运行。
//1中使用了 jsonwebtoken
模块来创建jwt token,创建的规则来自Apple的文档。注意在签名的时候,模块会自动添加时间戳,因此可以不用显示的添加iat
。
//2中创建了一个payload,相关的键值见文档。
//3表示创建一个http/2的请求,其中4个键值都是必须的。
//4表示对”response”的响应,如果jwt token或者某个参数有误,这里可以得到相应的提示,具体的错误代码可以参考这里。
//5就是把payload信息发送给APNs。
当项目启动后,点击网页中的超链接,会看到控制台显示APNs的response,紧接着App就收到一条推送消息。
以上是Provider和APNs通信的过程,那为什么Apple要引入使用token的基于http/2的方式呢?
考虑原先向APNs发送推送消息,无法得到反馈。对于需要知道消息送达率的情形,这是很头疼的事情。而且一旦因为某些原因,发送某一条消息失败,会导致接下来的消息都发送失败。现在和APNs的通信基于http/2,该协议最大的特点是多路复用,也就是Provider和APNs之间不会频繁的断开/连接:使用ping可以检测并维持当前的连接;而且有明确的response响应。
第二,原先使用证书的通信方式虽然安全,但是考虑到Provider和APNs之间仅仅是通信,并不(或是很少)涉及业务。使用token的话,就跨越了某个App的限制,对于有多个App的开发者就可以共享Provider。
第三,token虽然使用私钥加密,但是还是需要识别App和发送者,而jwt就可以添加一些非隐私的消息来识别不同的App。
补充
- payload的数据大小现在最大可以4kb。
- 当获得了一个token后,在有效期内(1个小时),使劲的用,不要再去请求新的token
- 如果需要发送的消息内容很多很大,可以创建多个连接分别发送,以提高性能
- Provider的实现可以参考 apns
Comments