diff --git a/.github/workflows/pr-open-check.yml b/.github/workflows/pr-open-check.yml index 6f2bd1d1..d109fb4e 100644 --- a/.github/workflows/pr-open-check.yml +++ b/.github/workflows/pr-open-check.yml @@ -15,7 +15,7 @@ jobs: - uses: wagoid/commitlint-github-action@v4 - uses: actions/setup-node@v2 with: - node-version: '16' + node-version: '18' check-latest: true - name: Get yarn cache directory path id: yarn-cache-dir-path diff --git a/docs/.vuepress/public/images/api-231017/event_subscription.png b/docs/.vuepress/public/images/api-231017/event_subscription.png new file mode 100644 index 00000000..0f6677bb Binary files /dev/null and b/docs/.vuepress/public/images/api-231017/event_subscription.png differ diff --git a/docs/develop/api-v2/dev-prepare/interface-framework/event-emit.md b/docs/develop/api-v2/dev-prepare/interface-framework/event-emit.md index 113a248b..39f1f9af 100644 --- a/docs/develop/api-v2/dev-prepare/interface-framework/event-emit.md +++ b/docs/develop/api-v2/dev-prepare/interface-framework/event-emit.md @@ -1,5 +1,5 @@ -# 事件订阅与异步通知 +# 事件订阅与通知 @@ -7,10 +7,76 @@ 当用户在QQ平台内的一些行为操作或某些接口的有异步返回通知确认机制的场景的时候,QQ 会通过"事件"的方式,通知到开发者服务器,开发者可自行根据具体事件通知来进行下一步响应。譬如用户跟机器人发消息,用户添加机器人好友,机器人被拉入群聊等等事件。 ::: +## Webhook方式 -## WebSocket 方式 +**当前方式灰度中,仅灰度用户可使用** 其它用户请使用 [websocket方式](#websocket方式) -通过 `WebSocket` 建立与QQ后台的长链接通信管道,当需要事件通知的时候QQ后台通过 `WebSocket` 连接下发事件到开发者服务器上。 +QQ机器人开放平台支持通过使用HTTP接口接收事件。开发者可通过[管理端](https://q.qq.com)设定回调地址,监听事件等。 + +### 数据结构 + +#### Payload + +网关的上下行消息采用的都是同一个结构,如下: + +```json +{ + "op": 0, + "d": {}, + "t": "GATEWAY_EVENT_NAME" +} +``` + +##### OpCode + +`opcode` 含义如下: + +| **CODE** | **名称** | **客户端行为** | **描述** | +| --- | --- | --- | --- | +| 0 | Dispatch | Receive | 服务端进行消息推送 | +| 12 | HTTP Callback ACK | Reply | 仅用于 http 回调模式的回包,代表机器人收到了平台推送的数据 | +| 13 | 回调地址验证 | Receive | 开放平台对机器人服务端进行验证 | +| 14 | 回调地址验证 ACK | Reply | 机器人服务端响应开放平台的验证请求 | + + +### 签名校验 +机器人服务端需要对回调请求进行签名验证以保证数据没有被篡改过。 +[签名算法](opcode.md) + +### 回调地址及事件监听配置 + +开发者需要提供一个HTTPS回调地址。并选定监听的事件类型。开放平台会将事件通过回调的方式推送给机器人。 +event_subscription + +开发者配置回调地址时,开放平台会对回调地址进行验证。机器人服务端需要按格式返回签名信息。签名算法同上。 机器人服务端需要在 3 秒内响应200或204,表示接受到事件。 +* 请求结构 + +| **字段** | **描述** | +| --- |------| +| plain_token | 要计算hash的字符串 | +| event_ts | 时间戳 | + +* 返回结果 + +| **字段** | **描述** | +| --- |-------------| +| plain_token | 要计算hash的字符串 | +| signature | 签名 | + +例如 + +回调验证请求: +```json +{"d": {"plain_token": "qgg8vlvZRS6UYooatFL8Aw","event_ts": 1654503849680},"op": 13} +``` +返回结果: +```json +{"plain_token": "qgg8vlvZRS6UYooatFL8Aw","signature": "23a89b634c017e5364a1c8d9c8ea909b60dd5599e2bb04bb1558d9c3a121faa5"} +``` + +## WebSocket方式 + +通过 `WebSocket` 建立与QQ机器人开放平台的长链接通信管道,当需要事件通知的时候QQ后台通过 `WebSocket` 连接下发事件到开发者服务器上。 开发者需要维护 `WebSocket` 长链接的状态,包括连接状态维护、登录鉴权、心跳维护、断线恢复重连等。 @@ -134,7 +200,7 @@ wss://api.sgroup.qq.com/websocket/ ```json { "op": 1, - "d": 251 // null + "d": 251 } ``` @@ -182,7 +248,7 @@ wss://api.sgroup.qq.com/websocket/ 事件和位移的关系如下: -```yaml +``` GUILDS (1 << 0) - GUILD_CREATE // 当机器人加入新guild时 - GUILD_UPDATE // 当guild资料发生变更时 diff --git a/docs/develop/api-v2/dev-prepare/interface-framework/sign.md b/docs/develop/api-v2/dev-prepare/interface-framework/sign.md new file mode 100644 index 00000000..f3bd1948 --- /dev/null +++ b/docs/develop/api-v2/dev-prepare/interface-framework/sign.md @@ -0,0 +1,75 @@ +# 安全和授权 + +开发者需要对每一次回调请求,根据回调中的签名等信息验证请求者身份,避免安全隐患。目前签名算法使用Ed25519。 + +## 安全凭证 + +开发者平台的 Bot Secret 用于加密签名字符串和服务器端验证签名字符串的密钥。用户必须严格保管安全凭证,避免泄露。 + +## 验证签名 + +### 1. 签名验证参数 + +| 字段名 | 说明 | 参考值 | +|-----------------------|---------------------------|-----------------| +| X-Signature-Ed25519 | HTTP Header 中透传 Signature | 3ecd***(64字节) | +| X-Signature-Timestamp | HTTP Header 透传的签名时间戳 | 1636373772 | +| HTTP Body | HTTP 请求中 Body 值 | {"msg":"hello"} | + +### 2. 验证签名过程 + +以下代码以Go语言为例,引用 `crypto/ed25519` 包实现 `Ed25519` 算法 + +- 根据开发者平台的 Bot Secret 值进行repeat操作得到签名32字节的 seed ,根据 seed 调用 Ed25519 算法生成32字节公钥 + +```go + // 根据botSecret进行repeat操作后得到seed值计算出公钥 + seed := botSecret + for len(seed) < ed25519.SeedSize { + seed = strings.Repeat(seed, 2) + } + rand := strings.NewReader(seed[:ed25519.SeedSize]) + publicKey, _, err := ed25519.GenerateKey(rand) +``` + +- 获取 HTTP Header 中 X-Signature-Ed25519 的值进行 hec (十六进制解码)操作后的得到 Signature 并进行校验 + +```go + // 取HTTP header中X-Signature-Ed25519(进行hex解码)并校验 + signature := req.Header.Get("X-Signature-Ed25519") + if signature == "" { + return false + } + sig, err := hex.DecodeString(signature) + if err != nil { + return false + } + if len(sig) != ed25519.SignatureSize || sig[63]&224 != 0 { + return false + } +``` + +- 获取 HTTP Header 中 X-Signature-Timestamp 的和 HTTP Body 的值按照 timestamp+body 顺序进行组合成签名体msg + +```go + // 取HTTP header中 X-Signature-Timestamp 并校验 + timestamp := req.Header.Get("X-Signature-Timestamp") + if timestamp == "" { + return false + } + // 按照timstamp+Body顺序组成签名体 + var msg bytes.Buffer + msg.WriteString(timestamp) + var body bytes.Buffer + // copy body into buffers + _, err = io.Copy(&msg, io.TeeReader(r.Body, &body)) + if err != nil { + return false + } +``` + +- 根据公钥、Signature、签名体调用 Ed25519 算法进行验证 + +```go + ed25519.Verify(publicKey, msg.Bytes(), sig) +```