业务事件回调
推送回调接口概述
1. 概述蓝信开放平台提供一套业务事件的订阅功能,应用如果需要关注蓝信平台发生的某些事件,可以通过回调事件订阅的方式来获取蓝信平台业务事件的通知。目前提供的事件订阅列表主要包含:蓝信平台人员的添加,修改,删除; 分支的添加,修改,删除;ISV应用的安装,卸载;自然人对应用机器人的消息回复等。如果应用订阅了某个事件,当事件发送后,蓝信开放平台会以HTTP POST请求的方式将事件内容以JSON格式推送到应用服务实现的回调接口。2. 使用场景
- 应用需要及时同步蓝信平台内的组织架构数据变化。例如OA应用可能需要及时获取蓝信平台组织内分支和人员的变化情况来及时响应相关的业务流程。
- 应用需要及时响应蓝信平台内的用户的交互请求。例如应用客服机器人需要及时获取蓝信平台内自然人用户向应用机器人发出的消息请求,以便进行及时的回复响应。
- ISV应用需要关心某个租户组织对应用的安装和卸载动作。例如根据租户组织的安装和卸载动作进行组织权限的处理。
以上描述是使用场景举例,应用可根据实际业务场景订阅不同的回调事件进行相关的业务回调事件处理。 订阅事件列表参考订阅事件列表
3. 对接事件订阅回调流程
应用如果需要订阅平台相关回调事件,需要完成几个工作:
- 应用服务需要实现一个接收蓝信开放平台回调事件推送请求的接口,应用侧需要实现的回调事件接收接口定义参见订阅事件回调接口
- 将推送回调事件接收接口的地址注册到蓝信开发者中心,应用详情页的回调事件管理的 “订阅事件回调地址”。
4. 配置推送回调地址和事件订阅
登陆蓝信开发者中心,打开应用详情-->事件回调,配置“订阅事件回调地址”,并保存。
蓝信开放平台到应用的回调,目前使用AES对称加密的方式保证数据传输的安全。页面中的”回调密钥“和”回调签名令牌“用于回调接口请求数据的加密和签名,应用需要将这两个参数配置到应用服务端,用于对回调接口数据进行验签和解密。具体签名和解密算法参见消息加解密说明

事件列表点击"编辑"进行事件订阅然后保存:

5. 接收并响应事件
当应用订阅的事件发生时, 蓝信会通过HTTP POST请求将事件以JSON格式数据推送到应用配置的回调地址接口。应用收到该事件推送请求后,需要在3秒内以HTTP 200的状态码响应该请求,否则,蓝信会分别在5分钟, 1小时,6小时的时间间隔后重新推送事件,最多重试3次。因为有重试,应用收到事件回调推送后需要根据事件ID进行去重处理,详见订阅事件回调接口。最后一次失败后,蓝信会将事件进行持久化,并提供接口供应用查询和该应用相关的所有失败事件列表。详细接口参见:订阅事件查询接口
回调事件列表
| 订阅事件描述 | 订阅事件类型定义 | 使用说明 |
|---|---|---|
| 人员回复应用号消息 | account_message | 可以支持应用和人员的一对一交互 |
| 分支创建 | dept_create | 可用于组织架构同步 |
| 分支变更 | dept_modify | 可用于组织架构同步 |
| 分支删除 | dept_delete | 可用于组织架构同步 |
| 人员创建 | staff_create | 可用于组织架构同步 |
| 人员变更 | staff_modify | 可用于组织架构同步 |
| 人员删除 | staff_delete | 可用于组织架构同步 |
| 电话追确认 | telephone_track | 电话追逻辑,用户确认收取动作通知应用 |
| 应用安装 | app_install_org | 某个组织安装了应用,将组织ID通知到应用 |
| 应用卸载 | app_uninstall_org | 某个组织卸载了应用,将组织ID通知到应用 |
| 数据读取范围配置变更 | data_scope | 应用在组织内的数据读取范围配置更新 |
| 用户登出蓝信客户端 | user_logout | 可用于应用侧清理人员登录状态,防止重放攻击 |
| 人员给机器人的私聊消息回复 | bot_private_message | 只有应用开启机器人能力后该事件订阅选项才可见 |
| 人员@机器人的群聊消息回复 | bot_group_message | 只有应用开启机器人能力后该事件订阅选项才可见 |
应用号消息回复
人员回复应用号消息 (type="account_message")
数据示例:|参数|类型 |描述 | |----------|----------|------------------------------------------------------------------------| | from | string |发送回调消息的人员ID | | msgType | string |消息类型,值为 text, image, video, file, voice, position, card, sticker等 | | msgData | json obj|消息内容对象 |
文本消息
| 参数 | 类型 | 描述 |
|---|---|---|
| content | string | 消息内容 |
| sendTime | string | 消息发送时间戳,精确到微秒 |
{
"from": "524288-xxx",
"msgType": "text",
"msgData": {
"text": {
"content": "this is a text",
"sendTime": "1540377644020456"
}
}
}
图片消息
| 参数 | 类型 | 描述 |
|---|---|---|
| mediaIds | string array | 图片文件列表 |
| sendTime | string | 消息发送时间戳,精确到微秒 |
{
"from": "524288-xxx",
"msgType": "image",
"msgData": {
"image": {
"mediaIds": ["524288-xxx1", "524288-xxx2"],
"sendTime": "1540377644020456"
}
}
}
视频消息
| 参数 | 类型 | 描述 |
|---|---|---|
| mediaIds | string array | 视频文件列表 |
| sendTime | string | 消息发送时间戳,精确到微秒 |
{
"from": "524288-xxx",
"msgType": "video",
"msgData": {
"video": {
"mediaIds": ["524288-xxx1", "524288-xxx2"],
"sendTime": "1540377644020456"
}
}
}
文档消息
| 参数 | 类型 | 描述 |
|---|---|---|
| mediaIds | string array | 文档文件列表 |
| sendTime | string | 消息发送时间戳,精确到微秒 |
{
"from": "524288-xxx",
"msgType": "file",
"msgData": {
"file": {
"mediaIds": ["524288-xxx1", "524288-xxx2"],
"sendTime": "1540377644020456"
}
}
}
语音消息
| 参数 | 类型 | 描述 |
|---|---|---|
| mediaIds | string array | 语音文件列表 |
| sendTime | string | 消息发送时间戳,精确到微秒 |
{
"from": "524288-xxx",
"msgType": "voice",
"msgData": {
"voice": {
"mediaIds": ["524288-xxx1", "524288-xxx2"],
"sendTime": "1540377644020456"
}
}
}
; 位置消息
| 参数 | 类型 | 描述 |
|---|---|---|
| type | int | 坐标类型:0-火星坐标,1-GPS的经纬坐标 |
| latitude | double | 经度 |
| longitude | double | 纬度 |
| name | string | 位置名 |
| address | string | 详细地址信息 |
| link | string | 地理位置卡片的链接 |
| mediaId | string | 位置对应图片的ID |
| sendTime | string | 消息发送时间戳,精确到微秒 |
{
"from": "524288-xxx",
"msgType": "position",
"msgData": {
"position": {
"type": 1,
"latitude": 1233.220,
"longitude": 2333.212,
"name": "北京",
"address": "北京市朝阳区",
"link": "www.test.com",
"mediaId": "524288-xxx",
"sendTime": "1540377644020456"
}
}
}
名片消息
| 参数 | 类型 | 描述 |
|---|---|---|
| staffId | string | 人员ID |
| sendTime | string | 消息发送时间戳,精确到微秒 |
{
"from": "524288-xxx",
"msgType": "card",
"msgData": {
"card": {
"staffId": "staff_id",
"sendTime": "1540377644020456"
}
}
}
表情消息
| 参数 | 类型 | 描述 |
|---|---|---|
| stickerId | string | 表情ID |
| sendTime | string | 消息发送时间戳,精确到微秒 |
{
"from": "524288-xxx",
"msgType": "sticker",
"msgData": {
"sticker": {
"stickerId": "sticker_id",
"sendTime": "1540377644020456"
}
}
}
分支信息变更
分支创建 (type="dept_create")
数据示例:|参数|类型 |描述 | |------------|----------|-------------------| | deptId | string | 创建分支ID | | timestamp | string | 创建时间戳,精确到微秒 |
{
"deptId": "524288-xxxxx",
"timestamp": "123456789411"
}
分支变更 (type="dept_modify")
数据示例:|参数|类型 |描述 | |------------|----------|--------------------| | deptId | string | 变更分支ID | | timestamp | string | 变更时间戳,精确到微秒 |
{
"deptId": "524288-xxxxx",
"timestamp": "123456789411"
}
分支删除 (type="dept_delete")
数据示例:|参数|类型 |描述 | |------------|----------|--------------------| | deptId | string | 删除分支ID | | timestamp | string | 删除时间戳,精确到微秒 |
{
"deptId": "524288-xxxxx",
"timestamp": "123456789411"
}
人员信息变更事件
人员创建 (type="staff_create")
数据示例:|参数|类型 |描述 | |------------|---------|-------------------| | staffId | string |创建人员ID | | timestamp | string |创建时间戳,精确到微秒 |
{
"staffId": "524288-xxxxx",
"timestamp": "123456789411"
}
人员信息变更 (type="staff_modify")
数据示例:|参数|类型 |描述 | |------------|------|--------------------| | staffId | string |发生信息变更的人员ID | | timestamp | string |更新时间戳,精确到微秒 |
{
"staffId": "524288-xxxxx",
"timestamp": "123456789411"
}
人员删除 (type="staff_delete")
数据示例:|参数|类型 |描述 | |------------|---------|----------------| | staffId | string |删除人员ID | | timestamp | string |删除时间戳,精确到微秒 |
{
"staffId": "524288-xxxxx",
"timestamp": "123456789411"
}
电话追确认回调
电话追确认回调 (type="telephone_track")
数据示例:|参数|类型|描述 | |---------------|---------------------|--------------------------------| | transactionId | string | 事务ID, 唯一标识一次请求 | | attach | string | 透传数据(对应微应用的appId) | | caller | json obj | 发起呼叫的人员信息(结构见下表) | | callee | json obj | 被呼叫的人员信息(结构见下表) | | confirmType | int | 0- 取消, 1-确认 | | timestamp | string | 处理时间戳,精确到微秒 | | staffId | string | 人员Id | | mobilePhone | json obj | 手机号信息 | | mobilePhone.countryCode | int | 手机号 | | mobilePhone. number | string | 手机号 |
{
"transactionId": "524288-xxxxx",
"attach":"xxxxxx",
"caller": {
"staffId": "524288-xxxxxxx",
"mobilePhone": {
"countryCode": 86,
"number": "12345678902"
}
},
"callee": {
"staffId": "524288-xxxxxxx",
"mobilePhone": {
"countryCode": 86,
"number": "12345678902"
}
},
"confirmType":1,
"timestamp": "1234567890"
}
应用安装,卸载与配置更新
应用安装 (type="app_install_org")
数据示例:|参数|类型 |描述 | |------------|----------|--------------------| | orgId | string | 组织ID | | orgName | string | 组织名称 | | timestamp | string | 事件时间戳,精确到微秒|
{
"orgId": "524288",
"orgName": "组织名称",
"timestamp": "123456789411"
}
应用卸载 (type="app_uninstall_org")
数据示例:|参数|类型 |描述 | |------------|----------|--------------------| | orgId | string | 组织ID | | orgName | string | 组织名称 | | timestamp | string | 事件时间戳,精确到微秒 |
{
"orgId": "524288",
"orgName": "组织名称",
"timestamp": "123456789411"
}
数据读取范围配置变更 (type="data_scope")
数据示例:|参数|类型 |描述 | |------------|----------|--------------------| | deptIds | string array | 分支ID列表 | | timestamp | string | 事件时间戳,精确到微秒 |
{
"deptIds": ["234583-XXXXXX","234583-YYYYYY","234583-ZZZZZZZZ"],
"timestamp": "123456789688"
}
用户登录状态通知
用户登出蓝信客户端 (type="user_logout")
数据示例:|参数|类型 |描述 | |------------|----------|--------------------| | staffId | string | 人员ID | | deviceId | string | 设备ID | | timestamp | string | 事件时间戳,精确到微秒 |
{
"staffId":"234583-xxxxx",
"deviceId":"设备id",
"timestamp":"12345678899"
}
智能机器人回复事件
自然人用户给智能机器人的私聊消息回复(type="bot_private_message")
数据示例:|参数|类型 |描述 | |------------|----------|--------------------| | from | string | 发送回复消息给智能机器人的人员openId | | entryId | string | 应用入口ID,大部分应用默认只有一个入口,可以忽略该字段 | | msgType | string | 消息类型,值为 text, image, video, file, voice, position, card, sticker等| | msgData | object | 消息内容对象,具体消息格式参照上面的公号消息回复格式|
{
"from": "524288-xxx",
"entryId":"xxx-xxx-xxx",
"msgType": "text",
"msgData": {
"text": {
"content": "this is a text",
"sendTime": "1540377644020456"
}
}
}
自然人用户@智能机器人的群聊消息回复(type="bot_group_message")
数据示例:|参数|类型 |描述 | |------------|----------|--------------------| | groupId | string | 群openId | | from | string | @智能机器人并发送回复消息的人员openId | | entryId | string | 应用入口ID,大部分应用默认只有一个入口,可以忽略该字段 | | msgType | string | 消息类型,值为 text, image, video, file, voice, position, card, sticker等| | msgData | json obj | 消息内容对象,具体消息格式参照上面的公号消息回复格式|
{
"groupId": "524288-xxx",
"from": "524288-yyy",
"entryId":"xxx-xxx-xxx",
"msgType": "text",
"msgData": {
"text": {
"content": "@智能机器人",
"sendTime": "1540377644020456"
}
}
}
订阅事件回调接口
接口说明: 该接口由第三方应用实现,接口地址需要注册到蓝信开发者中心。应用订阅的事件触发后,蓝信开放平台发起POST请求将事件以JSON数据方式推送到该接口。
开放平台调用应用回调接口时,有3秒左右的超时时间设置,如接口调用在设置的时间内没有返回,或接口返回其他错误,蓝信开放平台会认为接口调用失败并尝试重试,重试次数最多3次,分别在第一次回调失败后的5分钟,1小时,6小时。因为事件有重复回调,应用侧需要根据事件ID进行去重处理。最后一次充实失败后,失败事件会进行持久化,应用可以通过订阅事件查询接口接口查询应用相关的失败事件列表。
如果应用侧回调接口的业务逻辑处理所需时间较长,建议应用侧回调接口实现采用异步方式,尽快返回开放平台的接口调用,然后通过异步方式处理由回调事件触发的应用侧业务。
请求方式:POST (HTTPS),Content-Type: application/json请求地址:http(s)://callback?timestamp=TIMESTAMP&nonce=NONCE&signature=SIGNATUREquery参数说明(仅当第三方应用回调地址是 http 的时候):
| 参数 | 必须 | 说明 |
|---|---|---|
| timestamp | 是 | 发送回调消息的时间 |
| nonce | 是 | 一个随机值 |
| signature | 是 | 计算后的签名,签名计算方式参考消息加解密说明 |
当第三方回调地址是 http/https 的时候 ,请求数据会按照 消息加解密说明 加密成一个字符串):
解密前:
{
"dataEncrypt": "XXXXXXXX"
}
解密后:
{
"dataEncrypt": {
"random": "3vVNtlYYLTuAMiWQclQac0hPWqwm6HpxVJBay7QSU0a",
"length": 179,
"appId": "2990080-14155776",
"orgId": "2990080",
"events": [{
"id": "816b1029261c76a058c75c2f7f9083b8",
"eventType": "dept_create",
"data": {
"deptId": "524288-8euatzH7z7XFPgn0xbtP6B92pcagy",
"timestamp": "1680075586163886"
}
}]
}
}
请求参数字段说明:|参数|类型 |必须|说明 | |----------|----------|--------|----------| | random | string | 是 | 随机字符串,唯一标识一次请求 | | length | int | 是 | 表示events 的 JSON 字符串长度 | | appId | string | 是 | 应用ID | | orgId | string | 是 | 组织ID | | events | obj array| 是 | 事件列表 | | events.id | string | 是 | 回调消息去重的id。第三方可以用这个ID进行消息去重。 | | events.eventType| string | 是 | 事件类型,具体定义参见 回调事件类型格式定义 | | events.data | string | 是 | 事件具体结构,具体定义参见 回调事件类型格式定义)|
返回参数字段说明:
第三方回调服务接口收到请求后需要发送应答响应包。不需要做加密处理。
| 参数 | 描述 |
|---|---|
| errCode | 0: 第三方正确进行接收并且解析正常(包括解密); -1:解密失败 ; -2:计算签名失败 ; -3:数据反序列化失败 ; -4:其他类型错误 |
| errMsg |
返回数据示例:
业务正常返回:
{
"errCode":0 ,
"errMsg":"ok"
}
## 消息加解密说明
- 蓝信回调接口的签名和加密
对第三方应用回调地址,目前不强求支持 HTTPS。
因此为保证回调过程中的源端可信以及传输数据可靠性,在 回调 URL 参数中引入签名计算过程,在回调请求报文体中的内容引入加密过程。
应用接收到蓝信开平的回调请求后,需要:
- 对回调数据进行签名验证,可以防止对回调接口的非法请求,DDOS攻击等行为。应用的签名验证是建议操作,不是强制要求。
- 对回调的加密数据进行解密。应用的回调数据解密是必须的,否则应用拿不到明文的回调数据。
应用对回调请求数据进行解密时,需要使用回调密钥(aesKey),对签名进行验证时,需要使用回调签名令牌(signToken)。回调密钥和回调签名令牌是开发者在蓝信开发者中心创建应用并配置回调地址后获得。这两个参数需要作为配置参数配置到应用服务端,并注意保密,避免泄漏。
- 回调接口签名计算过程
生成签名的过程需要传递四个参数,包含了 签名token、时间戳、随机字符串,加密后的消息体
dev_data_signature=sha1(sort(token、timestamp、nonce、dataEncrypt))。
| 参数 | 必须 | 说明 |
|---|---|---|
| token | 是 | 在蓝信应用开发中心配置回调地址的时候指定的签名参数 |
| timestamp | 是 | 时间戳 |
| nonce | 是 | 随机值 |
| dataEncrypt | 是 | 见下文的消息加解密过程 |
生成签名包含以下两个步骤:
1、用sort函数将四个参数(均是字符串)按照参数值字母字典顺序进行从小到大排序,排序好后组成一个完整的字符串。
2、对排序好后的字符串按照SHA1 进行编码,编码的方式是把每字节散列值打印为%02x(即16进制,C printf语法)格式,全部小写 。生成的签名会在回调URL中传递到第三方,参考 回调接口定义 回调接口定义
- 消息体加解密(参考 回调接口定义 )
dataEncrypt = Base64_Encode( AES_Encrypt[random(16B) + eventsLen(4B) + orgId + appId + events ] )
AES加密的buf是一个JSON体序列化后的数据,详细JSON结构参考 回调接口定义
- 消息体解密
对密文BASE64解码
aes_msg=Base64_Decode(dataEncrypt)
使用AESKey做AES解密
rand_msg=AES_Decrypt(aes_msg)
-
加解密库:
java: <a href="/build-in/server-api/callback/pushcallback/msgcrypto-1.3.jar" download="msgcrypto-1.3.jar">msgcrypto-1.3.jar</a>
golang: <a href="/build-in/server-api/callback/pushcallback/msgcrypto.go" download="msgcrypto.go">msgcrypto.go</a>
python: <a href="/build-in/server-api/callback/pushcallback/msgcrypto.py" download="msgcrypto.py">msgcrypto.py</a>
-
具体调用实例请参考:<a href="/build-in/server-api/callback/pushcallback/lanxin-app-demo-java.tar.gz" download="lanxin-app-demo-java.tar.gz">Demo(java)</a>
注意事项:
1 , 应用若使用 java 的加解密库实现加解密时,此处采用的 Aes Key 的 256 Bit 方式会抛出异常:Illegal key size ( 受美国对软件出口限制的影响 )。
解决方案: 通过下载以下的资源对应替换运行环境的 jdk 与 jre 下的两个jar包: local_policy.jar 和 US_export_policy.jar 。
JDK6对应资源、 JDK7对应资源、 JDK8对应资源
Go 语言签名算法示例代码如下:
package testing
import (
"crypto/sha1"
"fmt"
"sort"
"testing"
)
func TestGenCallbackSign(t *testing.T) {
token := "31a4a1aa-cffc-4aca-9ef6-0497edf7fbed"
timestamp := "1646790230854428120"
nonce := "Rzem0rlz19e6GZuZuFKyDzaxiS4baaqn8uvxVnntXKS"
dataEncrypt:= "abcdefg"
signature := GenerateSignature(token, timestamp, nonce, dataEncrypt)
fmt.Println(signature)
return
}
// 数据签名计算
func GenerateSignature(token, timestamp, nonce, dataEncrypt string) (sign string) {
// 先将参数值进行排序
// 排序完毕进行SH1哈希
params := make([]string, 0)
params = append(params, token)
params = append(params, dataEncrypt)
params = append(params, timestamp)
params = append(params, nonce)
sort.Strings(params)
return Sha1Sign(params[0] + params[1] + params[2] + params[3])
}
//sha1哈希
func Sha1Sign(s string) string {
h := sha1.New()
h.Write([]byte(s))
//最终的哈希结果作为字节切片获取
bs := h.Sum(nil)
// SHA1值通常以十六进制格式打印,使用`%x`格式将哈希结果转换为十六进制字符串。
return fmt.Sprintf("%x", bs)
}
Go 语言解密算法示例代码如下:
package testing
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"testing"
)
func TestDecryptMsg(t *testing.T) {
//密文
dataEncrypt := "Iyh39ROwCyFhr5YPzQQF3cNsBbYpEZ9d+MbVzqUsyItgQMuhMbLZovpZb2kS0XAn7k8H/yUkxO5DQTUmGf4Xrhg5E/WukVddrhxV2V5VTr48+9SDrHpkWYK2Vr6lh4hb31wGfTLI+JV3L65Ep9+Mx124ZbK2K9Lo2jn6BUyU++6VhE0MKyeewrw00QM/b3KZzjXsMsf6tU/vtOazefC0OaAj9F0cuU+m8E3qArjqlDbdSup292e3h1nZpf9A6xGfhh/6KEJOn04VBiP4xN+uqCHQMjYM7Qj/0GRssksV7aeMEfBgMjVwtv0ymMpKhabs6S5j8FnvdPN3KzeS9CjebnvBrzIWnbhituz6951/XDW8OTxkRoa3sCW32ywMzyF9oKBvLR8lAtb9+Is9c2HHkXnEi0FRs/ZJkxTm+NvgxJdZT3epV3QnlHzkbR7QZf9XLd62QaRUNnkcgdhZs3nwihWsJ3oWXyFQ2/8d+I/TWX0="
//AES密钥
aesKey := "RDNBMkZCNkFDMThERjFDNkNFMjVFRDBEMjc4NkRERjM"
msg, _ := DecryptMsg(dataEncrypt, aesKey)
//解密后的明文结果
fmt.Println(msg)
}
//解密密文
func DecryptMsg(dataEncrypt, aesKey string) (string, int) {
//base64解码
decode, err := base64.StdEncoding.DecodeString(dataEncrypt)
if err != nil {
return "", -1 //BASE64解码失败
}
//解码后长度小于AES的BlockSize,默认16
if len(decode) < aes.BlockSize {
return "", -3
}
byteKey, err := base64.StdEncoding.DecodeString(aesKey + "=")
if err != nil {
return "", -1
}
block, err := aes.NewCipher(byteKey)
if err != nil {
return "", -2
}
blockSize := block.BlockSize()
blockMode := cipher.NewCBCDecrypter(block, byteKey[:blockSize])
plantText := make([]byte, len(decode))
blockMode.CryptBlocks(plantText, decode)
plantText = PKCS7UnPadding(plantText)
return string(plantText), 0
}
func PKCS7UnPadding(plantText []byte) []byte {
length := len(plantText)
unpadding := int(plantText[length-1])
return plantText[:(length - unpadding)]
}
Java 语言签名算法示例代码如下:
import java.security.NoSuchAlgorithmException;
import java.security.MessageDigest;
import java.util.Arrays;
class Example {
public static void main(String[] args) throws NoSuchAlgorithmException {
String token = "31a4a1aa-cffc-4aca-9ef6-0497edf7fbed";
String nonce = "Rzem0rlz19e6GZuZuFKyDzaxiS4baaqn8uvxVnntXKS";
String timestamp = "1646790230854428120";
String dataEncrypt= "abcdefg";
final String[] arrayStrs = { token, timestamp, nonce, dataEncrypt};
Arrays.sort(arrayStrs);
String sTemp = "";
for (final String s : arrayStrs) {
sTemp += s;
}
final MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(sTemp.getBytes());
final byte[] digest = md.digest();
String signature = "";
for (final byte b : digest) {
signature += String.format("%02x", b);
}
System.out.println(signature);
return;
}
}
Java 语言解密算法示例代码如下:
import javax.crypto.spec.IvParameterSpec;
import java.util.Arrays;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.Cipher;
import org.apache.commons.codec.binary.Base64;
class Example {
private static Base64 base64 = new Base64();
public static void main(String[] args) throws NoSuchAlgorithmException {
//原始密文
String dataEncrypt= "Iyh39ROwCyFhr5YPzQQF3cNsBbYpEZ9d+MbVzqUsyItgQMuhMbLZovpZb2kS0XAn7k8H/yUkxO5DQTUmGf4Xrhg5E/WukVddrhxV2V5VTr48+9SDrHpkWYK2Vr6lh4hb31wGfTLI+JV3L65Ep9+Mx124ZbK2K9Lo2jn6BUyU++6VhE0MKyeewrw00QM/b3KZzjXsMsf6tU/vtOazefC0OaAj9F0cuU+m8E3qArjqlDbdSup292e3h1nZpf9A6xGfhh/6KEJOn04VBiP4xN+uqCHQMjYM7Qj/0GRssksV7aeMEfBgMjVwtv0ymMpKhabs6S5j8FnvdPN3KzeS9CjebnvBrzIWnbhituz6951/XDW8OTxkRoa3sCW32ywMzyF9oKBvLR8lAtb9+Is9c2HHkXnEi0FRs/ZJkxTm+NvgxJdZT3epV3QnlHzkbR7QZf9XLd62QaRUNnkcgdhZs3nwihWsJ3oWXyFQ2/8d+I/TWX0=";
//AES密钥
String aesKey = "RDNBMkZCNkFDMThERjFDNkNFMjVFRDBEMjc4NkRERjM";
final byte[] decodes = base64.decode(dataEncrypt);
final byte[] byteKey = base64.decode(aesKey + "=");
final Cipher cipher;
try {
cipher = Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(2, new SecretKeySpec(byteKey, "AES"), new IvParameterSpec(Arrays.copyOfRange(byteKey, 0, 16)));
final byte[] encrypted = Base64.decodeBase64(dataEncrypt);
final byte[] encrpBytes = cipher.doFinal(encrypted);
final byte[] replyMsgBytes = GetPKCS7UnPadding(encrpBytes);
String msg = new String(replyMsgBytes, "UTF-8");
//解密后的明文结果
System.out.println(msg);
} catch (GeneralSecurityException | UnsupportedEncodingException e){
e.printStackTrace();
return;
}
return;
}
public static byte[] GetPKCS7UnPadding(final byte[] encrpBytes) {
final int elength = encrpBytes.length;
int cnt = encrpBytes[elength - 1];
if (cnt < 1 || cnt > 32) {
cnt = 0;
}
return Arrays.copyOfRange(encrpBytes, 0, elength - cnt);
}
}
Python 语言签名算法示例代码如下:
#!/usr/local/bin/python3
import hashlib
def generate_signature(token, timestamp, nonce, encrypt_data):
params = [token, encrypt_data, timestamp, nonce]
params.sort()
h = hashlib.sha1(''.join(params).encode("utf-8"))
return h.hexdigest()
if __name__ == '__main__':
token = "31a4a1aa-cffc-4aca-9ef6-0497edf7fbed"
timestamp = "1646790230854428120"
nonce = "Rzem0rlz19e6GZuZuFKyDzaxiS4baaqn8uvxVnntXKS"
encrypt_data = "abcdefg"
sign = generate_signature(token, timestamp, nonce, encrypt_data)
print(sign)
Python 语言解密算法示例代码如下:
#!/usr/local/bin/python3
## $ pip install pycryptodome
import base64
from Crypto.Cipher import AES
def decrypt(key, content):
key = base64.b64decode(key + "=").decode("utf-8")
encrypt_bytes = base64.b64decode(content)
key_bytes = bytes(key, encoding='utf-8')
cipher = AES.new(key_bytes, AES.MODE_CBC, bytes(key[:AES.block_size], encoding='utf-8'))
decrypt_bytes = cipher.decrypt(encrypt_bytes)
result = str(decrypt_bytes, encoding='utf-8')
result = pkcs7unpadding(result)
return result
def pkcs7unpadding(text):
length = len(text)
unpadding = ord(text[length - 1])
return text[0:length - unpadding]
if __name__ == '__main__':
aes_key = "RDNBMkZCNkFDMThERjFDNkNFMjVFRDBEMjc4NkRERjM"
encrypt_data = '5A/cI322pghOwnRCBoMZmOPjhzpZIdNmtW1Q05oG4z8L8lwIca2kIjrrwfGxlhJOk2LmLsdSLGRNQekNp8icYvd0W7vu7/hqL18wpYRgng0hvjUyUOBtpytU1qWwqyOaAIt9NwzJGq3emSlWhFMle/GnJqNer3vwyZ/IftfJ5mdG3qX02OLXV6cLEz3FhuhJLfLRUjmn2ZhCLv6+v3S+agdsYIU700sivpYW2bleG7AfaMz6uCyo0/EtXOjo+Ba3NnNuPd/mnwUo5raTOynj6SaLnpLJLCqZ56wtQeFuxYIetooOcv122DGM8t6Dg9oy8+1H7ZKGAzHjw9sBjg+2v5QEPodpgNl7bhBqbtNCxRUokkcLwbM7jawm9pVBkErj9Hh59zXtFCkka6ExCPo9/p/AA8+Tda/4r1KNnGDjw/pGsCt5m5AC1R+ub2Z35FyENXHP7tb9z5qn5eqthCUVg512PGCrE1GAEK8Gp7S4aTCrU7fQPh9QTXTxnpLiDFIrQUO6pTXaEmWhGz+KISOC5A=='
decrypt_data = decrypt(aes_key, encrypt_data)
print(decrypt_data)
订阅事件查询接口
接口说明: 当应用订阅的事件通过应用回调接口推送失败后,蓝信开放平台会对失败事件进行持久化并提供本接口允许应用对失败的回调事件列表进行查询。
特别说明:一个事件只允许查询一次,通过查询接口完成查询后,事件会标记删除,再次查询时将不再返回
请求方式:POST (HTTPS),Content-Type: application/json请求地址:/v1/callback/events/fetch?app_token=APP_ACCESS_TOKEN&query参数说明:|参数|必须|说明 |
|-----------|----------|--------------------------------------|
| app_token | 是 | 应用访问TOKEN |
数据请求示例:
{
"eventType":"staff_create",
"pageSize":100,
"eventOrgId":524288
}
请求参数字段说明:|参数|类型|必须|说明 | |------------|---------|-----------|------------------------------| | eventType | string | 否 | 事件类型,可选,不提供时默认全部 | | pageSize | int | 否 | 分页数据返回数据长度 ,可选,不提供时默认长度为100,每个失败事件只能被查询一次,再次查询时不会返回重复数据 | | eventOrgId | int | 否 | 事件组织ID,可选,不提供时默认全部 |
返回参数字段说明:|参数|类型|说明 |
|----------|-------------------|--------------------------|
| total | int | 当前接口返回的事件总数量 |
| hasMore | bool | 是否还有更多事件需要查询 |
| events | json obj | 事件列表 |
| events.id | string | 唯一标识一个事件的事务ID |
| events.eventType | string | 事件类型,一个字符串,参考推送回调事件列表 |
| events.data | json obj | 事件具体结构,参考 回调事件类型格式定义 |
返回数据示例:
业务正常返回:
{
"errCode": 0,
"errMsg": "ok",
"data": {
"total": 2,
"hasMore": false,
"events": [
{
"id": "1655870600187758983",
"eventType": "staff_create",
"data": {
"staffId": "524288-XXXXXXX",
"timestamp": "123456789411"
}
},
{
"id": "1655870600187794087",
"eventType": "staff_modify",
"data": {
"staffId": "524288-YYYYYYYYY",
"timestamp": "123456789422"
}
}
]
}
}
业务异常返回:
{
"errCode": 错误码 ,
"errMsg": 对应的统一错误码描述
}
拉取回调接口概述
1. 概述蓝信客户端和应用相关的部分动态数据展示需要通过应用提供的动态数据拉取接口获得,例如蓝信工作台应用红点或应用待处理任务角标。该回调拉取接口规范了应用提供动态数据拉取接口的方式。应用可根据需要决定是否提供数据拉取接口。2. 使用场景
蓝信客户端工作台页面或应用快捷入口页面会展示应用相关的部分动态数据,例如蓝信工作台应用红点或应用待处理任务角标。蓝信开放平台提供应用事件推送接口(接口定义参见:应用事件推送接口)允许应用将应用相关的部分数据推送到蓝信客户端。 当客户端不在线时,应用事件推送接口不能将数据实时送达蓝信客户端,等蓝信客户端网络恢复后,之前的事件推送动作会触发客户端的一个查询请求,因为应用事件推送的相关数据不会在蓝信开放平台进行存储,所以如果应用需要支持蓝信客户端离线状态下的应用事件送达,则应用需要实现一个查询接口,满足蓝信客户端对部分应用事件数据的查询请求。相关流程如下图:

3. 对接拉取回调接口流程
应用如果需要蓝信客户端从应用获取用户在应用内的动态数据,需要完成以下几个工作:
- 应用服务需要实现一个供蓝信开放平台拉取某个用户在应用内的动态数据的查询接口,应用侧需要实现的拉取回调事件查询接口定义参见拉取回调接口定义
- 将拉取回调事件接查询口的地址注册到蓝信开发者中心,应用详情页的回调事件管理的 “动态数据拉取地址”。
4. 配置拉取回调地址
登陆蓝信开发者中心,打开应用详情-->事件回调,配置“动态数据拉取地址”,并保存。

拉取回调接口定义
接口说明: 拉取回调地址由第三方应用提供。蓝信开放平台发起POST请求到第三方。
该接口允许的最大耗时为3秒,要求应用必须在3秒内返回数据,否则平台会按接口调用异常处理。
请求方式:POST (HTTPS),Content-Type: application/json请求地址:http(s)://callback?timestamp=TIMESTAMP&nonce=NONCE&signature=SIGNATUREquery参数说明(仅当第三方应用回调地址是 http 的时候):
| 参数 | 必须 | 说明 |
|---|---|---|
| timestamp | 是 | 发送回调消息的时间戳 |
| nonce | 是 | 一个随机值 |
| signature | 是 | 计算后的签名(签名计算方式参考消息加解密说明 |
请求数据示例(msg_encrypt的值会按照 消息加解密说明 进行加密成一个字符串):
解密前:
{
"dataEncrypt": "XXXXXXXX"
}
解密后:
{
"dataEncrypt": {
"random": "xxx",
"length": 1024,
"orgId": "ORGID",
"appId": "APPID",
"events": [
{
"id": "xxxxxxxx",
"entryId": "67a00245-8c6d-4382-a0bf-9a673cb23e8a",
"eventType": "app_changes",
"version": 1605693953610320,
"staffId": "524288-SDFESFWXXFSSSSSS"
}
]
}
}
| 参数 | 类型 | 必须 | 说明 |
|---|---|---|---|
| random | string | 是 | 用于加解密 |
| appId | string | 是 | 应用ID |
| orgId | string | 是 | 组织ID |
| length | string | 是 | 表示events 的 JSON 字符串长度 |
| events | obj array | 是 | 请求事件列表数据 |
| events.id | string | 是 | 回调消息去重的ID。第三方可以用这个ID进行消息去重。 |
| events.entryId | stirng | 否 | 入口ID,针对蓝图子应用ID或多入口应用的入口ID,为空时忽略即可。 |
| events.staffId | stirng | 否 | 如果该事件是关于指定的staffId,则该参数为必填项。 |
| events.eventType | string | 是 | 事件类型,目前支持的预定类型为:工作台红点 - "app_changes",三方应用需要根据这个事件类型决定返回何种类型数据内容。 |
| events.version | int64 | 否 | 数据的版本号,记录数据变更的时间戳,精确到微秒,例如:1605693953610320。 应用可以根据这个版本号判断蓝信侧该数据和应用侧该数据差异。应用根据情况也可以忽略该数据。 |
返回参数字段说明:
第三方回调服务接口收到请求后需要发送应答响应包。不需要做加密处理。
| 参数 | 类型 | 必须 | 描述 |
|---|---|---|---|
| errCode | int | 是 | 0: 第三方正确进行接收并且解析正常(包括解密); -1:解密失败; -2:计算签名失败; -3:数据反序列化失败; -4:其他类型错误 |
| errMsg | string | 是 | 错误信息描述 |
| data | json obj | 是 | 返回数据部分 |
| data.events | obj array | 是 | 事件列表 |
| events.id | string | 是 | 对应结果事件ID |
| events.staffId | string | 否 | 人员ID,可选字段,取决于请求参数中是否设置了staffId |
| events.entryId | stirng | 否 | 入口ID,针对蓝图子应用ID或多入口应用的入口ID,为空时忽略即可。 |
| events.eventType | string | 是 | 事件类型,目前支持的预定类型为: 工作台红点 - "app_changes",三方应用需要根据这个事件类型决定返回何种类型数据内容。 |
| events.eventData | string | 是 | 事件具体结构序列化之后的json字符串,预定义事件类型接口需要参考本文档,应用自定义事件由应用自己确定。; 工作台红点数据(eventType:"app_changes"):; {; " unread":8, //int类型; " hasNew":false //bool类型,true/false; }; 说明:字段unread为工作台应用代办事项角标未读数,字段hasNew为工作台应用代办事项红点。红点和角标未读数不可同时工作,如果使用未读数效果,则hasNew必须填false。如果使用红点效果,则unread字段必须填0。 |
| events.version | int64 | 否 | 可选字段,数据的版本号代表的是数据的修改情况,要求是个记录数据变更的时间戳,精确到微秒, 例如:1605693953610320。 该字段描述的是数据变化的时间点,目的是解决高并发场景下,数据事件拉取乱序时的数据一致性保证,可以让接收者确定数据更新的先后顺序,避免后到的旧数据覆盖先到的新数据。当事件数据不存在变化的先后顺序时,可不填,该字段不填时,平台会默认取接口调用时的当前系统时间。 |
返回数据示例:
业务正常返回:
{
"errCode": 0,
"errMsg": "ok",
"data": {
"events": [
{
"id": "xxxddsfsfsgdsffw",
"entryId": "524288-aabbccdd",
"staffId": "524288-xxxdfsfsfs",
"eventType": "app_changes",
"eventData": "{\"hasNew\":false,\"unread\":8}",
"version": 1605693953610320,
}
]
}
}