sensai2.0

335
15

序. Web Push

Push API是w3c新推出的浏览器API,基于Web Push协议旨在实现WebApp的离线推送客户端,通过离线消息推送使得WebApp在用户体验等方面更加接近Native应用。

虽然截止目前(2018-04),Push API还是处于Draft阶段,但是大部分现代浏览器已经支持Push API

兼容性参考: https://caniuse.com/#search=push

Push API文档地址参考: https://www.w3.org/TR/push-api/

Web Push协议[RFC8030]参考https://tools.ietf.org/html/rfc8030


1. 推送流程

实现消息推送需要三个组成模块:客户端(UA),推送服务器(Push Service),应用服务器(Application Server)

首先需要客户端向推送服务器进行发起消息订阅请求,成功之后客户端将返回的订阅资源(消息通道、凭证、客户端标识等数据)发给应用服务器(即业务方的服务端),应用服务端存储当前客户端相关数据。

当服务端需要向特定客户端推送消息时,便可以取出存入的数据,并发送消息到推送服务器中,最终推送服务器将消息推给客户端。

示意如下:

    +-------+           +--------------+       +-------------+
    |  UA   |           | Push Service |       | Application |
    +-------+           +--------------+       |   Server    |
        |                      |               +-------------+
        |      Subscribe       |                      |
        |--------------------->|                      |
        |       Monitor        |                      |
        |<====================>|                      |
        |                      |                      |
        |          Distribute Push Resource           |
        |-------------------------------------------->|
        |                      |                      |
        :                      :                      :
        |                      |     Push Message     |
        |    Push Message      |<---------------------|
        |<---------------------|                      |
        |                      |                      |

​2. 身份验证与加密

虽然​启用Web Push的前提是站点必须是https的(避免了最基本的中间人劫持问题),但是浏览器的Push API是通过js调用并获取的

如果消息订阅资源通过明文返回,那么势必造成订阅资源对其他js脚本可见。一旦页面存在XSS漏洞或不可靠的统计js就会造成订阅资源信息的泄露,所有第三方可以通过向Push Service发送fake message从而使得客户端收到fake push。

Web Push定义了一套加密与客户端身份识别的算法,基于ECDH椭圆加密算法进行加解密。

这里不具体讨论ECDH的原理,只需要知道这是一种非对称加密算法即可。

我们知道非对称加密需要公钥和私钥,公钥仅仅能够对数据进行加密却无法解密,只有私钥才能对数据进行解密。

所以客户端持有公钥,并且在订阅消息Subscribe的时候将公钥传给Push Service,Push Service将生成的身份验证信息用公钥加密后再返回给客户端,最终客户端拿到并发送给服务端的是加密后的资源。

而只有用户的Application Server持有私钥,当需要给客户端推送消息时,Application Server会将加密后的数据用私钥进行解密,从而获取并生成Push Service的身份验证token,有了解密后的token信息,才能向Push Service发出Push Message的合法请求。

​最终通过非对称加密,解决了任何第三方都能向Push Service发送fake message的安全问题。


关于加密部分更多可以参考:

ECDH wiki: https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman

* webpush-encryption文档 https://webpush-wg.github.io/webpush-encryption/padding/draft-ietf-webpush-encryption.html#rfc.section.1


3. Push API客户端实现

​由于Web Push需要在关闭网站的时候进行推送,而只有ServiceWorkerScope是独立于网页运行的,所以不难想到Push API是在ServiceWorkerScope下运行。

Push Client需要做两件事:订阅Push Service以及监听消息推送,浏览器已经在内部做了基本的实现,用户只需要

调用ServiceWorkerRegistration.PushManager下的相关api即可。

消息订阅方法需要调用serviceWorkerRegistration.pushManager.subscribe方法

navigator.serviceWorker.ready                                                       
.then(serviceWorkerRegistration => serviceWorkerRegistration.pushManager.subscribe({
    userVisibleOnly: true,                                                          
    applicationServerKey: urlBase64ToUint8Array(applicationServerKey),              
}))                                                                                 
.then(subscription => {                                                             
    // Subscription was successful                                                 
    // create subscription on your server
    // ...                                           
    return true;                     
})                                                                                  
.catch(e => {                                                                       
    if (Notification.permission === 'denied') {                                     
        // The user denied the notification permission which                        
        // means we failed to subscribe and the user will need                      
        // to manually change the notification permission to                        
        // subscribe to push messages                                               
        console.warn('Notifications are denied by the user.');                                          
    } else {                                                                        
        // A problem occurred with the subscription; common reasons                 
        // include network errors or the user skipped the permission                
        console.error('Impossible to subscribe to push notifications', e);                                                
    }                                                                               
});                                                                                 

其中urlBase64ToUint8Array方法是将base64编码格式的公钥转换成Uint8Array,实现如下:

function urlBase64ToUint8Array(base64String) {                     
    const padding = '='.repeat((4 - base64String.length % 4) % 4); 
    const base64 = (base64String + padding)                        
        .replace(/\-/g, '+')                                       
        .replace(/_/g, '/');                                       
                                                                   
    const rawData = window.atob(base64);                           
    const outputArray = new Uint8Array(rawData.length);            
                                                                   
    for (let i = 0; i < rawData.length; ++i) {                     
        outputArray[i] = rawData.charCodeAt(i);                    
    }                                                              
    return outputArray;                                            
}                                                                  
注册消息订阅服务器只需要调用subscribe方法,然而这里我们无法指定Push Service的url,也无法自己开发自己的推送服务器,不同的浏览器使用的是各自实现的推送服务。

比如Chrome返回的推送地址:

https://fcm.googleapis.com/fcm/send/cNXYYk9lkgg:APA91bFo-foRJ-_31yyPfQaSGhGREwPzLmJ1CU7VjAVcHBsiY_EqbrUlZppTCMGjjb7k9APuq_Cd5U_tV5wps9EZh5WF1B8JA_LMCFb165oTYcJSPaWv4nM81Jhp2UXALT5vnMgQmLMo

这个地址明显在国内是访问不到的,所以Push API在国内的使用场景十分有限。

subscribe返回的值包括endpoint以及加密后的key以及auth信息,这些数据需要发给业务方的服务端并存储下来。


接收push message方法只需要在ServiceWorkerScope下监听push事件:

// service-worker scope
self.addEventListener('push', function (event) {
  // 推送消息事件
  // PushEvent
});
收到PushEvent后,一般做的是发一个Notification给用户,从而实现在不打开网页的情况下给用户发送通知的效果。

demo如下:

self.addEventListener('push', function (event) {
    if (!(self.Notification && self.Notification.permission === 'granted')) {
        return;
    }

    const sendNotification = body => {
        // you could refresh a notification badge here with postMessage API
        const title = "Web Push example";
        // 发一个弹窗通知
        return self.registration.showNotification(title, {
            body,
        });
    };    

    if (event.data) {
        const message = event.data.text();
        event.waitUntil(sendNotification(message));
    }
});                                                                           
当然除此之外也可以做一些别的事情,比如ServiceWorker收到通知后更新本地资源缓存等。


​4. Application Server的实现

业务方的​后端主要做的事情也是两件: 存储客户端发送来的{endpoint, auth, key}数据以及根据业务需求给指定的客户端push message

Application Server已经有了完善的各种语言的开源实现,可以在此基础上二次开发。

具体可以参考 https://github.com/web-push-libs

​至于密钥对的生成,可以使用https://github.com/web-push-libs/web-push里提供的命令行工具,如下图所示:

最后下了一个https://github.com/web-push-libs/web-push-php​的demo放到本站下试了一下,果然在国内不开梯子工具是收不到推送的Notification的。


测试地址: https://powerpigger.cc​​

回复

对话列表

×