AE在WebPush技术中的探索之路

之前几个月在做的WebPush项目,最早接入Firebase的方案,看了一下官方的demo觉得这个需求应该很简单,直到做的过程中遇到fcm token失效的各种坑,以及AE多语言站点的问题,可以说是一路坎坷。

由于国内连不上谷歌服务,而谷歌的Chrome市场占有率最大,所以国内基本上很少看到有做WebPush的网站,也正因此关于WebPush的中文资料少之又少,可以借鉴的网站都是国外的。我们前端接了Firebase的SDK由于体积太大了,移动端也被迫下线。

在ATA上搜了一下相关技术,看到在16年就有AE的同学做过WebPush,从ODPS上的一张表看出18年波哥也基于Firebase做过WebPush,最后应该都无疾而终了,顿时觉得WebPush远比想象的复杂。

所以国庆回家几天,研究了一下WebPush的规范,以期能在深陷在Firebase的坑中得到解脱。

1、浅谈Push消息

Push消息是移动互联网的产物。作为移动端两大操作系统,iOS和Android有各自的消息推送服务,苹果有APNs(Apple Push Notification service),谷歌有GCM(Google Cloud Messaging)和FCM(Firebase Cloud Messaging),FCM是Firebase被Google收购后与谷歌结合的产物。移动互联网竞争失败者微软也有自己的MPNs。APNs和GCM都是系统级的推送能力,以APNs为例,iOS系统会有一个5223端口专门用于连接APNs服务。由于政策原因以及Android的开源生态,导致GCM在国内几乎无法使用。一般方案都是开发者自己在App的后台进程中与自己的服务端建立通信。但这样方案缺点是进程被kill后,push就收不到了,所以大家都利用App的进程拉活来保证后台进程活跃,这也导致了前几年Baidu PushAliPush、腾讯的信鸽友盟个推极光等各种推送联盟繁盛。各种自建通道需要维持长连接耗电耗内存耗流量,导致Android用户体验极差,这也促使工信部牵头建设统一推送联盟。由于利益关系,各大厂商都想主导这个系统,导致进展缓慢,于是国内各手机厂商都在建立自己系统的Push通道。但是各家厂商又都有自己的小算盘,毕竟有较大的人力物力投入,所以通道被商业化,比如下发速度等上面做一些文章,这也就与原始Push通道体验问题慢慢变味。另一方面,如果App要有非常好的推送体验,必须集成多家厂商的推送SDK,还有三方的推送SDK,这不仅有很大的开发压力还对自身App的体积是不小的考验。

Push消息

2、从AppPush到WebPush

在任何操作系统上浏览器都是使用最频繁的App,随着浏览器的性能提升,很多应用都没有独立的App,而是建立在Web上。WebApp的兴起也激发了WebPush的需求。

谷歌除了Android操作系统拥有市场最大占比,Chrome也在浏览器市场独占鳌头。苹果和谷歌一样都有自己的操作系统和浏览器,所以苹果的APNs和谷歌的FCM也最早支持了WebPush消息。

PC操作系统上,苹果的MacOS在APNs上与iOS建立了统一的推送服务。微软从Win8开始就借鉴了苹果建立了应用商店和WNS(Windows Push Notification Services),WNS就是想统一Windows和WinPhone的消息推送,无奈WP在移动端竞争失败,而PC上Win32传统应用已经根深蒂固,商店应用难以推广。Windows自家的Edge浏览器的推送就是基于WNS的。

WebPush

Mozilla的Firefox仿佛是独立于三界之外的产物,但是却推送了整个WebPush规范的制定,Mozilla的云服务(MCS)也提供了规范中的Push Service的实现autopush。目前FCM已经支持了Push规范Edge浏览器也在2018年4月支持了Push标准。苹果的Safari由于其一贯的封闭式发展,目前仍然未支持Push API规范。

3、WebPush规范

WebPush规范分为三部分:WebPush的推送方式(RFC8030),WebPush消息的加密(RFC8291),客户端浏览器与应用服务的识别(RFC8292)。

3.1、WebPush工作方式

RFC8030中将Push服务分为三个角色:UA就是用户浏览器,Push Service就是FCM、autopush、MNS等云服务,App Server就是我们自己开发的应用。

WebPush工作方式

整个过程分为一下几步:

1、用户在浏览器中点击接受订阅后,会向Push Service发起订阅请求;

2、Push Service会为用户的浏览器分配一个订阅信息;

3、开发者需要在拿到订阅信息后上传到自己的App Server上;

4、后面应用服务器就可以使用浏览器的订阅信息要求Push Service向用户推送相应的Push消息了。

看上去很简单的步骤,这里面其实涉及了很多问题:

  • Push Service会与若干个浏览器建立非常多的TCP连接,高效的管理这些连接对Push Service是个挑战。好在现在I/O multiplex技术很成熟,Java中已经有Netty这样的网络库了。

  • App Server向Push Service推送消息后,如果用户浏览器没打开,需要等到用户浏览器打开才能收到消息。消息的实时性可能收到影响,这个时候可能需要设置一下消息的过期时间(TTL)。

  • 浏览器订阅信息的过期问题,Push规范提供了这个字段,但是实际多久过期取决于Push Service,WebPush标准没有强制订阅信息到期。

    这里有统计各浏览器订阅信息时长的统计:https://blog.pushpad.xyz/2018/09/web-push-subscription-age-affects-delivery-rates/

  • Push消息的安全性,首先要求应用站点必须基于HTTPS协议,另外为了保证中间人Push Service拿到消息后无法将消息传给第三方或者篡改消息,在RFC8291中定义了WebPush消息加密的标准。

  • App Server如何向Push Service标识自己,从而保证拿到用户订阅信息后只有App Server能发送给制定的浏览器,其他人通过改订阅信息无法发送消息给用户。RFC8292中就定义了应用服务器识别协议VAPID(Voluntary Application Server Identification)。有了VAPID就不再需要遵循FCM的推送步骤,也不需要在Firebase中创建项目拿到gcm_sender_id

3.2、VAPID协议与消息加密协议

对开发者来说,VAPID协议是WebPush的关键,因为这个规范定义了App Server与Push Service的握手以及确定Push Service往哪个客户端发送消息。

这个过程其实也很简单,前提是你对公钥加密算法比较熟悉:

1、首先需要创建一对公/私钥,公钥由运行在浏览器端的Web站点持有,私钥由我们的App Server持有。

2、当用户在浏览器点击接受订阅后,Web程序需要将公钥作为options参数传入到PushManager.subscribe()方法中。

3、调用subscribe方法后,浏览器会向Push Service发送订阅请求注册该浏览器。请求中包括Web程序的公钥,Push Service会返回一个Token用于标识该浏览器。下图谷歌的FCM注册设备时的请求报文:

subscribe

4、拿到这个token,我们就可以拼接出一个http endpoint向push service发送消息了。

比如上面的fcm就是的拼接出来的endpoint为:

1
https://fcm.googleapis.com/fcm/send/fDWFicUHqAw:APA91bFuV2sz0Uz55b1WkLdfXMcUDlA9w7dZZJyvn1XMTPhW0rqMND83gjQ_s6gP60xZn-ubnroyCxJpr4Ejkk-H_F23H0pCwOpyKjEDp3QZdPJlNM1-RmOxWtcOFGjSI5MiKE3F4Qtn

5、同时浏览器还会生成一对用于加密Push消息的公/私钥。

Subscription.getKey(name)方法可以获取两个Key:

1
2
3
4
5
// p256h是客户端生成的公钥,私钥会保存在浏览器中。
// 这样能保证App Server加密后的数据不会被Push Service解密
var key = subscription.getKey('p256dh');
// 客户端还会生成一个用于加密的认证信息
var auth = subscription.getKey('auth');

6、服务端存储了这endpoint、publicKey、auth三个信息后,就可以给客户端推送push了。这里AppServer的加密方式和HTTPS的有点类似,它不是使用客户端的PublicKey直接对Push消息加密,而是根据客户端的PublicKey生成一个类似于HTTPS会话密钥的对称密钥,再使用AES/GCM对称加密算法对消息体进行加密。这样结合了对称加密的高速与非对称加密的安全。

3.3、WebPushLib

要我自己实现VAPID和消息加密的这一套流程,那肯定得费九牛二虎之力的。不过我比较幸运WebPushLib最早在15年使用Javascript实现了这一套算法,后面陆续也有了其他语言的实现。我觉得16年AE的那个前辈没有成功的原因就是没有这么一个java库(因为Java的这个库是16年5月30号才开始开发的)。

有了WebPushLib一切就如拨云见日,但是AE还是有一个问题亟待解决。

4、AE的多站点授权问题

AE由于国际化的特性,每个站点都会有不同域名,比如主战是www.aliexpress.com,俄罗斯站点域名是aliexpress.ru,德国站点是de.aliexpress.com…。除此之外,AE业务很复杂,根据业务还会有my.aliexpress.comtrade.aliexpress.comsale.aliexpress.combest.aliexpress.com等域名。

Web Push Permission的权限是基于域名的,每个域名都会有自己的权限,这就可能导致同一用户会有多个订阅信息。推送时可能会给同一个用户推送重复的消息。

谷歌了一下multi origin web push,谷歌官方都说没有完美的解决方案。提到一个解决方案是根据浏设备信息(屏幕大小等数据)生成一个id,用作标识。这个我们前期也用了fingerprint生成了一个browserId,但是第一次用的时候囊括了所有的数据包括浏览器的插件和版本信息,导致browserId不稳定。

Multi Origin web push

后面受daraz的灵感,觉得可以可以通过二次弹窗跳到统一的域名进行授权。这个网站有几个Demo可以展示这种做法。

Refs:

浅谈Push消息推送:https://www.atatech.org/articles/173453

WebPush相关规范:https://tools.ietf.org/wg/webpush/

web-push-notifications-history-effectiveness-more:https://blog.resellerclub.com/web-push-notifications-history-effectiveness-more/

get-started-web-push-notifications-tutorial-demo:https://blogs.windows.com/msedgedev/2018/05/22/get-started-web-push-notifications-tutorial-demo/

WebPushDemo:https://webpushdemo.azurewebsites.net/

how-to-reduce-unsubscribe-rate-push-notification:https://blog.pushengage.com/how-to-reduce-unsubscribe-rate-push-notification/

web-push-subscription-age-affects-delivery-rates:https://blog.pushpad.xyz/2018/09/web-push-subscription-age-affects-delivery-rates/

sending-vapid-identified-webpush-notifications-via-mozillas-push-service:https://blog.mozilla.org/services/2016/08/23/sending-vapid-identified-webpush-notifications-via-mozillas-push-service/

web-push-interop-wins:https://developers.google.com/web/updates/2016/07/web-push-interop-wins

web-push-complex-integrations:https://documentation.onesignal.com/docs/web-push-complex-integrations