# 接收并处理事件

为了让开发者可以便捷地接收并处理事件,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": {}
}
1
2
3
4
5
6
7
8
9
10
11

# 事件处理方法

为了提升事件订阅的安全性,接收到Bosshi开放平台推送的事件后,可以进行安全校验。如果是加密事件,需要先解密事件,再解析事件详情。

# 事件解密

事件内容采用AES-256-CBC加密,加密过程:

  1. 使用SHA256对EncryptKey进行哈希得到密钥key;
  2. 使用PKCS7Padding方式将事件内容进行填充;
  3. 生成16个字节的随机数作为初始向量iv;
  4. 使用iv和key对事件内容加密得到encryped_event;
  5. 应用收到的密文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);
    }

}
1
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
}
1
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
1
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));
                }
        }
}
1
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
最后更新于 : 11/6/2024, 5:05:23 PM