# 接收并处理事件
为了让开发者可以便捷地接收并处理事件,Bosshi开放平台提供了 Java SDK、Golang SDK 和 NodeJS SDK。有关 SDK 的详细介绍,可以参考服务端 SDK 介绍(待开发)。
本文介绍基本的事件处理方法。接收到事件后,一般需要对事件进行安全校验和解密。
可以查看事件列表,了解目前支持订阅的所有事件。
如果应用没有及时接收到订阅的事件,可以在开发者后台 (opens new window),日志检索 > 事件日志检索 页面,查看日志信息,确认Bosshi开放平台是否推送了所订阅的事件。
# 事件推送逻辑
# 推送加密事件
如果配置了Encrypt Key,在进行业务逻辑处理前,需要先对事件解密。有关 Encrypt Key 配置方法的详细介绍,请参考(可选)配置 Encrypt Key。有关事件解密方法的详细介绍,请参考本文下方的事件解密。
# 推送周期和频次
订阅的事件发生时,Bosshi将会通过 HTTP POST 请求发送 JSON 格式的事件数据到预先配置的请求地址。
应用收到 HTTP POST 请求后,需要在 1 秒内以 HTTP 200 状态码响应该请求。否则Bosshi开放平台认为本次推送失败,并以 15s、5m、1h、6h 的间隔重新推送事件,最多重试 4 次。
从上述描述可以看出,事件重发的最长时间窗口约为 7.5 小时,请检查和处理在 7.5 小时内的重复事件。可以使用如下方式判断事件唯一性:
通过事件结构中的 event_id
字段判断事件唯一性。
# 事件推送顺序
为了保证用户的事件可用性以及内外部数据变化一致性,对于部分事件,开放平台使用了有序事件的形式进行推送。即在用户对前一事件接收成功后,才会推送下一事件。
对于有序事件,用户需要保证相应前后事件的正常消费,避免造成事件的阻塞或收到事件不及时。
# 事件结构
header.event_id
字段是事件的唯一标识。header.token
字段即 Verification Token。header.create_time
字段表示事件发送的时间,一般近似于事件发生的时间。header.event_type
字段表示事件类型。event
结构体记录的是事件的详细信息,不同事件的信息不同。
{
"header": {
"event_id": "f7984f25108f8137722bb63cee927e66",
"token": "066zT6pS4QCbgj5Do145GfDbbagCHGgF",
"create_time": "1603977298000000",
"event_type": "contact.user_group.created_v3",
"tenant_key": "xxxxxxx",
"app_id": "cli_xxxxxxxx"
},
"event": {}
}
2
3
4
5
6
7
8
9
10
11
# 事件处理方法
为了提升事件订阅的安全性,接收到Bosshi开放平台推送的事件后,可以进行安全校验。如果是加密事件,需要先解密事件,再解析事件详情。
# 事件解密
事件内容采用AES-256-CBC加密,加密过程:
- 使用SHA256对EncryptKey进行哈希得到密钥key;
- 使用PKCS7Padding方式将事件内容进行填充;
- 生成16个字节的随机数作为初始向量iv;
- 使用iv和key对事件内容加密得到encryped_event;
- 应用收到的密文encrypt为base64(iv+encryped_event)。
# 解密示例代码
# Java
package com.Bosshisuite.oapi.sample;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class Decrypt {
public static void main(String[] args) throws Exception {
Decrypt d = new Decrypt("test key");
System.out.println(d.decrypt("P37w+VZImNgPEO1RBhJ6RtKl7n6zymIbEG1pReEzghk=")); //hello world
}
private byte[] keyBs;
public Decrypt(String key) {
MessageDigest digest = null;
try {
digest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
// won't happen
}
keyBs = digest.digest(key.getBytes(StandardCharsets.UTF_8));
}
public String decrypt(String base64) throws Exception {
byte[] decode = Base64.getDecoder().decode(base64);
Cipher cipher = Cipher.getInstance("AES/CBC/NOPADDING");
byte[] iv = new byte[16];
System.arraycopy(decode, 0, iv, 0, 16);
byte[] data = new byte[decode.length - 16];
System.arraycopy(decode, 16, data, 0, data.length);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBs, "AES"), new IvParameterSpec(iv));
byte[] r = cipher.doFinal(data);
if (r.length > 0) {
int p = r.length - 1;
for (; p >= 0 && r[p] <= 16; p--) {
}
if (p != r.length - 1) {
byte[] rr = new byte[p + 1];
System.arraycopy(r, 0, rr, 0, p + 1);
r = rr;
}
}
return new String(r, StandardCharsets.UTF_8);
}
}
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
# Golang
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"strings"
)
func main() {
s, err := Decrypt("P37w+VZImNgPEO1RBhJ6RtKl7n6zymIbEG1pReEzghk=", "test key")
if err != nil {
panic(err)
}
fmt.Println(s) //hello world
}
func Decrypt(encrypt string, key string) (string, error) {
buf, err := base64.StdEncoding.DecodeString(encrypt)
if err != nil {
return "", fmt.Errorf("base64StdEncode Error[%v]", err)
}
if len(buf) < aes.BlockSize {
return "", errors.New("cipher too short")
}
keyBs := sha256.Sum256([]byte(key))
block, err := aes.NewCipher(keyBs[:sha256.Size])
if err != nil {
return "", fmt.Errorf("AESNewCipher Error[%v]", err)
}
iv := buf[:aes.BlockSize]
buf = buf[aes.BlockSize:]
// CBC mode always works in whole blocks.
if len(buf)%aes.BlockSize != 0 {
return "", errors.New("ciphertext is not a multiple of the block size")
}
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(buf, buf)
n := strings.Index(string(buf), "{")
if n == -1 {
n = 0
}
m := strings.LastIndex(string(buf), "}")
if m == -1 {
m = len(buf) - 1
}
return string(buf[n : m+1]), nil
}
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
# Python
import hashlib
import base64
from Crypto.Cipher
import AES
class AESCipher(object):
def __init__(self, key):
self.bs = AES.block_size
self.key = hashlib.sha256(AESCipher.str_to_bytes(key)).digest()
@staticmethod
def str_to_bytes(data):
u_type = type(b"".decode('utf8'))
if isinstance(data, u_type):
return data.encode('utf8')
return data
@staticmethod
def _unpad(s):
return s[:-ord(s[len(s) - 1:])]
def decrypt(self, enc):
iv = enc[:AES.block_size]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return self._unpad(cipher.decrypt(enc[AES.block_size:]))
def decrypt_string(self, enc):
enc = baseb64.b64decode(enc)
return self.decrypt(enc).decode('utf8')
encrypt = "P37w+VZImNgPEO1RBhJ6RtKl7n6zymIbEG1pReEzghk="
cipher = AESCipher("test key")
print("明文:\\n{}".format(cipher.decrypt_string(encrypt)))
# 明文:hello world
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
# C#
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace decrypt
{
class AESCipher
{
const int BlockSize = 16;
private byte[] key;
public AESCipher(string key)
{
this.key = SHA256Hash(key);
}
public string DecryptString(string enc)
{
byte[] encBytes = Convert.FromBase64String(enc);
RijndaelManaged rijndaelManaged = new RijndaelManaged();
rijndaelManaged.Key = this.key;
rijndaelManaged.Mode = CipherMode.CBC;
rijndaelManaged.IV = encBytes.Take(BlockSize).ToArray();
ICryptoTransform transform = rijndaelManaged.CreateDecryptor();
byte[] blockBytes = transform.TransformFinalBlock(encBytes, BlockSize, encBytes.Length - BlockSize);
return System.Text.Encoding.UTFGetString(blockBytes);
}
public static byte[] SHA256Hash(string str)
{
byte[] bytes = Encoding.UTFGetBytes(str);
SHA256 shaManaged = new SHA256Managed();
return shaManaged.ComputeHash(bytes);
}
public static void Main(string[] args)
{
string encrypt = "P37w+VZImNgPEO1RBhJ6RtKl7n6zymIbEG1pReEzghk=";
AESCipher cipher = new AESCipher("test key");
Console.WriteLine(cipher.DecryptString(encrypt));
}
}
}
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
# Node.js
const crypto = require("crypto");
class AESCipher {
constructor(key) {
const hash = crypto.createHash('sha256');
hash.update(key);
this.key = hash.digest();
}
decrypt(encrypt) {
const encryptBuffer = Buffer.from(encrypt, 'base64');
const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, encryptBuffer.slice(0, 16));
let decrypted = decipher.update(encryptBuffer.slice(16).toString('hex'), 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
encrypt = "P37w+VZImNgPEO1RBhJ6RtKl7n6zymIbEG1pReEzghk="
cipher = new AESCipher("test key")
console.log(cipher.decrypt(encrypt))
// hello world
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
← 申请权限 配置 Encrypt Key →