基于Node.js的APNS和Passbook服务

本文介绍iOS开发中常用到的两个服务APNS和Passbook基于Node.js实现的工具,虽然大部分公司可能目前不会选择Node.js开发后台,但对于广大iOS开发人员来说能够不依赖后台开发人员,自己写后台配合联调APNS或Passbook程序还是方便的多,甚至你可以在了解了一系列处理流程后指导一下后台开发人员完成开发,毕竟他们对苹果的服务不一定有你了解的多^_^。

express

在正式介绍这两个工具之前,先简单的介绍一下express,因为本文的Demo基于express框架。 Express是一个简洁灵活的node.js Web应用框架,它提供了一系列强大的特性帮助快速创建各种Web应用,有兴趣的可以进一步了解。本文demo中主要用到express中路由的管理和请求的封装。首先通过npm install -g express安装express包,安装完成后创建express应用express --ejs MyService,此时在当前目录下会创建一个MyService文件夹,里面有各种目录,编辑package.json文件,在dependencies中增加apnspassbook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "application-name",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node app"
},
"dependencies": {
"express": "3.1.0",
"mongodb": "1.2.14",
"ejs": "*",
"apns": "0.1.0",
"passbook": "*"
}
}

安装依赖包cd MyService & npm install, 安装完成后使用node app.js启动服务(默认服务地址:(http://localhost:3000),浏览器中输入该地址后出现Express页面说明服务创建成功。

APNS

首先介绍的是一款apns工具(https://github.com/neoziro/node-apns),可直接通过npm install apns安装。因为上面我们创建express应用时已经将apns作为了依赖包安装,所以此时你的服务中已经包含了apns模块。
大家知道APNS服务需要Apple开发证书,申请APNS证书步骤这里就不介绍。通过从iOS Dev Center下载的证书是.cer格式,本机导出来的密钥是.p12格式,而apns包只支持.pem的文件,所以需要进行格式转换:

1
2
openssl x509 -inform der -in push.cer -out cer.pem
openssl pkcs12 -in key.p12 -out key.pem -nodes

转换完后,将cer.pem key.pen放在./public/push/ 目录下。

创建两个服务uploadToken.do和sendMessage.do,在app.js中添加对应的路由:

1
2
3
4
5
//apns
app.get('/uploadToken.do', routes.uploadToken);
app.post('/uploadToken.do', routes.uploadToken);
app.get('/sendMessage.do', routes.sendMessage);
app.post('/sendMessage.do', routes.sendMessage);

然后在./MyService/routes/index.js文件中添加对应的服务:

1
2
3
4
5
6
7
8
9
10
11
12
//保存上送的apns deviceToken。
exports.uploadToken = function(req, res){
var token = req.param['deviceToken'];
if (!token) {
token = req.body['deviceToken'];
}
/**可将deviceToken保存到数据库
* 当然处理deviceToken外还可上传其他参数,如城市等,
* 推送消息时可以根据这些参数选择性推送。
*/
//...
}
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
//根据条件给设备推送消息
exports.sendMessage = function(req, res){
var apns = require('apns'), options, connection;
options = {
certFile: "./public/push/cer.pem",
keyFile: "./public/push/key.pem",
gateway : "gateway.sandbox.push.apple.com",
port : 2195,
debug : true
};
//找出符合推送条件的Token.
var tokens = getTokens(req);
var message = nil; //message to send.
for(var i = 0; i < tokens.length; i++) {
var notification = new apns.Notification();
notification.device = new apns.Device(token[i]);
notification.badge = 1;
notification.payload = {"description" : "node-apns test"};
notification.alert = message;
notification.sound = "dong.aiff";
connection.sendNotification(notification);
}
//set response...
}

以上只是介绍基本用法,主要是了解一下API,感兴趣可以进一步研究或优化。

Passbook

Passbook是苹果在iOS 6.0+ 的iPhone, iPod Touch推出的一项应用,可以将各个应用的电子票据保存到Passbook应用中。本文中使用passbook工具见(https://github.com/assaf/node-passbook),可通过npm install passbook安装。因为上面我们创建express应用时已经将passbook作为了依赖包安装,所以此时你的服务中已经包含了passbook模块。在使用Passbook之前也需要到iOS Dev Center请求相应的证书,主要是获取pass Type ID标识。服务器中需要使用的是Apple Worldwide Developer Relations Certification证书,密钥是Pass Type ID:pass.com.xxx.passbook对用的密钥,也都需要转换成.pem格式。

1
2
openssl x509 -inform der -in wwdr.cer -out wwdr.pem
openssl pkcs12 -in key.p12 -out key.pem -nodes

转换完后将wwdr.pem和key.pem文件保存到./public/passbook目录(注:passbook工具中固定了key和cer名字必须为key.pem、wwdr.pem,当然你可以修改passbook包变成可配置的)。

创建服务getPass.do和passbook.do,在app.js中添加对用的路由:

1
2
3
4
5
app.get('/getPass.do', routes.getPass);
app.post('/getPass.do', routes.getPass);
app.get('/passbook.do/*', routes.passbook);
app.post('/passbook.do/*', routes.passbook);
app.delete('/passbook.do/*', routes.passbook);

然后在/MyService/routes/index.js文件中添加对应的服务:

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
exports.getPass = function(req, res){
var passIdentifier = nil; //服务器生产pass唯一标识
var providerName = nil; //服务提供商
var createTemplate = require("passbook");
var template = createTemplate('eventTicket', {
passTypeIdentifier: "pass.com.esoftmobile.passbook", //后台配置,由客户端开发人员提供
teamIdentifier: "ES13NBDEV1", //后台配置,由客户端开发人员提供
serialNumber: passIdentifier, //pass唯一标识号,服务器生产
organizationName: "Esoft Mobile", //后台配置或客户端根据商户服务上送
description: "node passbook test", //服务器配置
backgroundColor: "rgb(206, 140, 53)", //服务器配置
foregroundColor: "rgb(255, 255, 255)", //服务器配置
formatVersion: 1,
authenticationToken: "vxwxd7J8AlNNFPS8k0a0FfUFtq0ewzFdc", //服务器生成,用于pass校验
webServiceURL: "http://localhost:3000/passbook.do" //服务器配置,最为pass与服务器之间关联的唯一接口
});
template.keys("./public/key/pass", "secret"); //pass生成证书和密钥,由客户端开发人员提供
template.loadImagesFrom("./public/images/pass"); //生成pass的图片素材,由服务器配置
//所有value值都可以通过客户端上送或服务器配置
//地理,时间、二维码信息
var pass = template.createPass({
locations:[{ "longitude" : -122.3748889,"latitude" : 37.6189722},
{ "altitude" : 10.0, "longitude" : -122.029, "latitude" : 37.331, relevantText : "距离机场10公里"}],
relevantDate: "2013-04-20T20:30-08:00",
barcode : {
"message" : "hello passbook",
"format" : "PKBarcodeFormatQR",
"messageEncoding" : "utf-8"
}
});
pass.headerFields.add('type', '奖卷', '1');
pass.primaryFields.add('provider', '', providerName);
pass.secondaryFields.add('expires', '有效期', '2013年4月23日~2013年5月4日');
var File = require('fs');
var file = File.createWriteStream('./public/tmp/pass.pkpass');
pass.pipe(file);
pass.on("end", function(){
res.header('Content-Type','application/vnd.apple.pkpass');
res.download('./public/tmp/pass.pkpass');
});
pass.on('error',function(error){
if(error){
console.log(error);
}
res.render('index', { title: 'MyService | pass create error!' });
});
}

(注:如果要生成boardingPass类型的pass,必须设置transitType,而transitType只能通过 pass.fields.boardingPass.transitType = "PKTransitTypeAir";方式设置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//pass中webServiceURL配置的服务地址,为pass安装后与服务器唯一的连接。
exports.passbook = function(req, res){
var method = req.method;
switch(method){
case 'DELETE': {
//pass删除时会以DELETE方式通知服务器,服务器可以删除相关记录。
//...
break;
}
case 'POST' : {
//pass安装成功后,会以POST方式通知服务器,并且参数pushToken为该pass对应推送Token。
//服务器可保存该pass对应的设备信息。
var pushToken = req.body['pushToken'];
break;
}
case 'GET' : {
//pass检查更新时以GET方式,服务器根据参数判断是由有更新,有更新可直接返回新生成的pass,没有更新可返回304。
break;
}
default : {
}
}
}

结束

本文所有例子和代码都未在生产环境使用过,所以可靠性和效率等方面还有待验证,有兴趣的朋友可以进一步研究。