消息推送有着十分广阔的应用场景:
新品上架,推送消息给用户,点击即进入商品详情页面。
用户很久没有进入站点了,推送消息告知这段时间站点的更新。
使用推送消息通知,能够让我们的应用像 Native App 一样,提升用户体验。
但是目前整体支持度并不高,在手机端更是只有安卓 Chrome57 支持。
如同淘宝卖家将商品送到用户家中需要依赖快递公司,服务器向浏览器推送消息,也需要推送服务(Push Service)的帮助。 下面让我们看看服务器,浏览器和推送服务这三者在推送过程中扮演的角色。
在订阅消息之前,浏览器需要得到用户授权,同意后才能使用消息推送服务。
显示以上对话框有两种方式:
在订阅之前先获取用户授权,使用Notification.requestPermission
如果不选择使用方法1,在正式订阅时浏览器也会自动弹出,对于开发者而言不需要显式调用
在第一种使用Notification.requestPermission
的方式中,由于通知API还不稳定,需要兼容新旧版本的返回值:
值得注意的是,当用户允许或者拒绝授权后,后续都不会重复询问。 想要更改这个设置,在 Chrome 地址栏左侧网站信息中如下:
如同卖家只有获取了买家的地址簿,才能委托快递公司发货。服务端作为消息来源,委托推送服务发送消息给订阅消息的浏览器,也必须知道浏览器的具体地址。
这个具体的地址是推送服务产生的,不同的服务端和不同的浏览器都会产生不同的地址。那么在订阅时服务端(也就是我们的应用)就需要一个唯一标识的身份。
这里就需要引入applicationServerKey
的概念,它又被称作VAPID,这就是我们应用的唯一标识。
生成applicationServerKey
的方法有两种:
在服务端使用 web-push 生成,在后续使用web-push发送消息一节中会详细介绍
访问https://web-push-codelab.appspot.com/快速生成
此时得到的applicationServerKey
是base64编码后的字符串,需要转换成UInt8Array
格式,才能作为订阅方法接受的参数。
另外要注意,生成applicationServerKey
的同时,会同时生成与之配对的私钥,用于后续服务端请求推送服务的安全验证(详见后续消息推送安全性一节),因此这个私钥是绝对不能暴露在页面中的。
拥有了服务端的唯一标识,浏览器可以开始向推送服务发起订阅请求了,有两点要注意:
请求推送服务的地址对于开发者而言是无法指定的,完全由浏览器决定。
在请求发送之前,浏览器已经生成了一个推送订阅对象(PushSubscription)。得到响应之后,会将推送服务生成的地址加入这个推送订阅对象中。
一个完整的推送订阅对象结构如下:
其中endpoint
就是推送服务返回的唯一标识用户设备的地址,而keys
是浏览器预先生成的,包含了用于安全验证信息,在后续向推送服务发送消息时会使用到(详见后续消息推送安全性一节)。
订阅消息的具体实现步骤如下:
注册 Service Worker
使用 pushManager 添加订阅,浏览器向推送服务发送请求,其中传递参数对象包含两个属性:
userVisibleOnly
,不允许静默的推送,所有推送都对用户可见,所以值为true
applicationServerKey
,服务器生成的公钥
得到推送服务成功响应后,浏览器将推送服务返回的 endpoint 加入推送订阅对象,向服务器发送这个对象供其存储
以上步骤对应的具体代码实现如下:
如果订阅时第二步pushManager.subscribe()
中缺少了applicationServerKey
参数,我们会得到这样的错误信息:
错误信息中前半段显而易见,但后半段中提到的gcm_sender_id
是什么呢?
GCM(Google Cloud Messaging)是 Google 早期的推送服务,现已更名为 FCM(Firebase Cloud Messaging),对于旧版本的 Chrome 浏览器,gcm_sender_id
相当于applicationServerKey
。
如果开发者想兼容这些老版本的浏览器,那么可以参考非标准浏览器的兼容措施。
在某些情况下,例如服务端请求推送服务,返回了推送服务失效错误码,此时需要取消订阅,代码实现如下:
现在我们有了买家的地址,需要委托快递公司发货。
所有推送服务都遵循统一的调用标准,好比快递公司有着全国统一的上门服务电话,这就是 Web Push Protocol 。
推送服务接到了服务器的调用请求,向设备推送消息,如果处于离线状态,消息将进入待发送队列,过期后队列清空,消息被丢弃。
下面介绍保证消息安全的原理,以及在实际使用中服务端的具体代码实现。
商品需要包裹封装,就连快递公司也无权拆开。
消息推送的安全体现在两方面:
推送服务确保调用来自可靠的服务端
推送消息内容只有浏览器能够解密,就算是推送服务也不行
下面简单介绍下保证安全性的原理,如果只关心实际项目中的具体实现,可以直接跳到使用web-push发送消息这一节。
服务器在调用推送服务时,需要额外发送请求头,例如 Authorization 和 Crypto-Key,首先介绍 Authorization。
JWT(JSON Web Token)提供了一种消息接收者验证发送者的方法。
Authorization 就包含了 JWT 格式的字符串: Authorization: 'WebPush <JWT Info>.<JWT Data>.<Signature>'
Authorization 的内容由三部分组成,使用.
连接,前两部分是使用base64编码后的JSON字符串:
JWT Info,指明了签名使用的加密算法
JWT Data,包含发送者的信息,推送服务的源地址,失效时间,和发送者的联系方式
签名,连接前两部分,服务端使用私钥加密。还记得之前添加订阅的时候,使用到的服务端生成的公钥吗,此处使用的正是与之配对的私钥
另外,请求头中还需要将公钥带给推送服务: Crypto-Key: p256ecdsa=<URL Safe Base64 Public Application Server Key>
这样,当推送服务收到服务端的调用请求时,使用公钥解密 Authorization 签名部分,如果匹配前两部分,说明请求来自可靠的服务端。
由于推送 API 的统一性,用户可能误发消息到不信任的推送服务,对消息进行加密可以确保只有浏览器端才能解密读取,防止将用户信息泄露给不合法的推送服务。
还记得最初用户订阅成功后,浏览器生成的推送订阅对象吗?里面包含了endpoint
,而加密过程会使用其中的keys
对象。
复杂的加密过程在这里就不展开介绍了。总之,只有订阅的浏览器能使用生成的私钥解密推送的消息。
现在,服务端可以向 endpoint 发送包含以上请求头的请求了,推送服务响应201
表示接受调用。 其余响应状态码如下:
429 Too many requests
400 Invalid request
404 Not Found 订阅过期,需要在服务端删除保存的推送订阅对象
410 Gone 订阅失效,需要在服务端删除保存的推送订阅对象,并调用推送订阅对象的unsubscribe()
方法
413 Payload size too large
服务端请求推送服务,需要涉及加密,设置请求头等复杂操作,使用web-push可以帮助我们解决大部分问题。
步骤如下:
使用 web-push 生成一对公私钥,还记得 pushManager 订阅时需要用到的applicationServerKey
吗,我们需要公钥publicKey
传递到订阅脚本所在的页面中。
调用setVapidDetails
为 web-push 设置生成的公私钥
之前订阅时浏览器已经将推送订阅对象发送到了服务端,此时从数据库中取出。
调用sendNotification
向推送服务发起调用请求,如果返回错误状态码,从数据库中删除保存的推送订阅对象。
以上步骤实现代码如下:
想象一个卖家常驻小区的派送员,收到快递公司的商品,上门按下买家的门铃。 Service Worker 就是扮演这样的角色,它监听 push 事件,显示通知。
使用消息中携带的数据,展示通知,此处省略了通知对象(Notification)的配置信息,示例代码如下:
至此,整个推送流程就结束了。对于开发者而言,只需要关心服务器和浏览器端的修改。
CMS(Content Management System)是网站内容管理系统简称, 互联网上每个网站(无论大小门户)其后台都由专业CMS系统支撑- Zoomla!逐浪CMS作为国内高端CMS与WEB应用典范,首创第3代CMS理念,专注底层核心技术研发,以云技术、创新精神构建行业新成就,提供从网站内核到电商、办公、移动一体化的开发体验!