webhook 的定义
来自于维基百科的定义
网络钩子是“用户定义的HTTP回调”。
网络钩子通常被某些事件激活,比如将代码推送到源或评论博客。
当此事件发生时,原网站将向为网络钩子配置的URL发送HTTP请求。用户可配置它们引发网页上的事件以调用另一个网站的行为。
此操作可为任何事件。网络钩子常用于激活持续集成系统的构建操作或用于提醒缺陷跟踪管理系统。
由于网络钩子使用HTTP,它们可以被无缝集成入网页服务而无需添加新的基础设施。
在笔者看来,通俗点来讲,当你的系统A想要接收系统B的一些事件消息,系统B定义了数据的格式,系统A需要在系统B中配置一个API,当系统B触发了系统A订阅的事件,则系统A配置的API会收到系统B的调用,从而进行其他逻辑的处理。
一般系统A提供的API是公网可以访问的,暴露在公网上意味着任何人都可以调用,那么就可能被别人伪造成系统B的请求,从而干一些危害系统的事情,所以一般系统B除了定义数据的格式,还要将如何验证来源是系统B的策略告知系统A,系统A再去验证来源,验证通过后,再处理其他的业务逻辑。
同时,系统A有可能有一段时间服务有异常,不能访问,那怎么保证数据不丢失,系统B在设计webhook时就还要考虑消息的确认和重试。
验证webhook的来源
以下,介绍两个笔者在项目中遇到的两种验证的方式
AWS的消息验证
亚马逊有个服务叫做SNS,简单通知系统,通过配置邮箱、或者 http/https endpoint 等可以获取订阅主题推送的消息,那么亚马逊是怎样做消息验证,证明此来源是来自亚马逊的呢?
这个链接是亚马逊提供的官方文档范例,感兴趣的读者可以阅读下。
AWS推送的数据 message
为JSON类型。
let message = JSON.parse(req.body);
message
里有一个属性为 SigningCertURL
,这是一个签名证书的下载url,我们需要根据这个url获取到证书,然后从证书中得到公钥 publicKey
。
const pem = require('pem');
let url = message.SigningCertURL;
superagent
.get(url)
.buffer(true)
.end((err, data) => {
if (err) {
console.log(err);
} else {
let pemStr = data.text;
pem.getPublicKey(pemStr, (err, publicKey) => {});
}
});
然后AWS提供了一个方法将 message
对象转化为了可以验证签名的 signBuffer
,这个方法就不列出来了
let signBuffer = getMessageBytesToSign(message);
message
对象中有一个 Signature
属性,是一个字符串的签名,然后用得到的 signBuffer
以及 publicKey
通过 SHA1
算法进行校验
const crypto = require('crypto');
const verify = crypto.createVerify('SHA1');
verify.update(signBuffer);
let isVerify = verify.verify(
Buffer.from(publicKey.publicKey),
Buffer.from(message.Signature, 'base64')
);
最后得到的 isVerify
是一个Boolean类型的值,就可以通过此值来判断有无验证通过了。
而在系统B中则将消息用密钥签名即可,通过RSA这种非对称加密更为安全。
const crypto = require('crypto');
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
});
const sign = crypto.createSign('SHA1');
sign.update('some data to sign');
sign.end();
const signature = sign.sign(privateKey);
const verify = crypto.createVerify('SHA1');
verify.update('some data to sign');
verify.end();
console.log(verify.verify(publicKey, signature));
shopify的验证签名
shopify是一个建站系统,有过跨境电商工作的经验的读者应该有所耳闻。
shopify提供了一个应用商店,在开发应用时与其系统做对接时用到了webhook的推送,shopify是怎样验证来源的呢?
shopify的校验比较简单采用了hmac生成消息摘要
HMAC是密钥相关的哈希运算消息认证码,HMAC运算利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。
shopify推送过来的消息中,一个是数据,一个是签名,签名放在了请求头里面
let message = JSON.stringify(req.body);
let providedHmac = req.headers['x-shopify-hmac-sha256'];
通过自己生成摘要然后跟请求头里面获取到的摘要进行比较,相同则验证正确。hmac需要提供一个密钥和一个消息作为输入,密钥是在开发shopify应用时提供的。
const crypto = require('crypto');
const generatedHash = crypto
.createHmac('sha256', shopifyConfig.apiSecret)
.update(message)
.digest('base64');
let hashEquals = generatedHash === providedHmac;
hashEquals
的真假值即是验证通过与否了。
这是系统A检验时所做的判断,而系统B也是按照这样的规则生成 providedHmac
放入 headers
即可。
这是两种验证的方式,笔者更倾向于第二种,比较简单,系统A和系统B需要各保管好密钥不被泄漏。
webhook的重试
网络传输波动,或者系统A正在升级、重启之类的,都会导致系统A的API暂时不能访问,而这时系统B发过来了消息,系统A却没有真正的接收到。
而系统B要怎么判断系统A到底有没有收到呢?一般情况下,简单一点的话,约定好,当系统B调用API时只要返回的http的状态码是200,则认为成功了,其他情况认为没有成功,则要一段时间后再去重试。
重试如何去设计呢?
shopify的重试策略是
Shopify has implemented a five second timeout period and a retry period for subscriptions. Shopify waits five seconds for a response to each request to a webhook. If there is no response, or an error is returned, then Shopify retries the connection 19 times over the next 48 hours. A webhook is deleted if there are 19 consecutive failures.
也就是说等接口5秒钟返回,5秒没返回或者返回错误则认为是失败,然后会在48小时内重试19次,重试19次不行的话就将这个webhook删除了
AfterShip的重试策略
AfterShip sent the events for each webhook URL with POSTs request. If the webhook URL doesn't return a 200 HTTPresponse code,
that POST request will be re-attempted up to 14 times in increasing intervals: 2^number_of_fail x 30s
e.g.
If the attempt fail, AfterShip will retry the 2nd attempt 30s later.
If the 5th attempts fail, AfterShip retry the 6th attempt 960s later
If the 14th attempts fail, AfterShip retry not send out that webhook any more.
这里对于系统B来讲还要考虑待重试的消息如何保存,如何在规定的时间内重试等,
一个简单的策略就是将下次推送的时间以及消息保存在数据库中,然后定时去获取下次推送时间小于当前时间的消息,再次推送,出错则更新下次推送的时间,成功则删除记录。
总结
笔者分享了一些工作中关于webhook的经验,希望对读者有所启发、帮助,本人能力有限,有什么问题欢迎评论或邮我。