增量数据推送
概述
数据推送是指在某个资源发生变化时,自动向订阅的应用发送变化相关的信息请求的功能。
配置
开发者将应用的订阅地址、Token 和关注的资源提交给希悦工作人员,我们在后台完成配置。
- 推送地址:一个可访问的公网地址,推送的信息会通过 HTTP POST 请求发送到该地址。在正常的情况下,接口应返回 2xx 状态码(通常为 200)。
- Token:一个字符串,用于参与验证推送请求的合法性。由开发者自行生成。
- 资源:开发者可以选择关注的资源,比如用户、权限等。
请求参数格式
请求方式:POST
Header 参数
| 参数名称 | 参数类型 | 参数说明 |
|---|---|---|
| X-Nonce | string | 随机字符串,用于签名 |
| X-Timestamp | integer | Unix 时间戳 |
| X-Signature | string | 签名信息 |
| X-School-Id | integer | 学校 ID |
Request Body
Content-Type: application/json
{
"id": "0190c2e4-1f00-7000-8000-000000000001",
"resource": "user",
"timestamp": "2024-04-15 14:25:32",
"events": [
{
"op": "created",
"pks": [1]
},
{
"op": "updated",
"pks": [2, 3]
},
{
"op": "deleted",
"pks": [4]
}
]
}
| 字段名称 | 字段类型 | 字段说明 |
|---|---|---|
| id | string | 本次推送的唯一 ID(UUID v7),开发者可以根据该 ID 进行幂等处理 |
| resource | string | 资源类型,用于标识本次推送所属的资源 |
| timestamp | string | 批次时刻(北京时间),格式为 2024-12-31 12:00:00 |
| events | array | 事件列表,按操作类型聚合 |
| events.op | string | 操作类型,可能的值 created,updated,deleted |
| events.pks | array | 受影响行的主键列表;单列主键为标量数组([1, 2]),复合主键为对象数组([{"a": 1, "b": 2}]),无主键时为空数组([]) |
推送示例如下:
POST https://yourdomain.com/
Content-Type: application/json
X-Nonce: 9xmas123
X-Timestamp: 1713153015
X-Signature: xxxx
X-School-Id: 1
{
"id": "0190c2e4-1f00-7000-8000-000000000001",
"resource": "user",
"timestamp": "2024-04-15 14:25:32",
"events": [
{
"op": "created",
"pks": [1]
},
{
"op": "updated",
"pks": [2]
}
]
}
请求认证
为保证推送请求的合法性,我们在请求中加入了签名信息,开发者可以通过验证签名信息来判断请求的合法性。
签名信息的生成方式如下:
- 创建一个字典,写入
nonce(取自X-Nonce)和timestamp(取自X-Timestamp,转换为整型) - 将 Request Body 解析为字典后,合并(merge)进上述字典(Body 中的同名 key 会覆盖 Header 的对应项,例如 Body 顶层的
timestamp会覆盖 Header 的X-Timestamp) - 对合并后的字典递归地按 key 字典序排序(ksort)
- 将排序后的字典编码成 JSON 字符串
- 使用 HMAC-SHA256 算法,以开发者配置的 Token 为密钥,对 JSON 字符串进行签名
- 将签名结果以十六进制字符串形式填入
X-SignatureHeader
签名示例:
预定义的 Token 值:87892dedaf483eeabed6c54e4335fbe5
收到请求如下:
POST https://yourdomain.com/
Content-Type: application/json
X-Nonce: bfcf312b
X-Timestamp: 1713162332
X-Signature: 6db829e0cdd7d677fbfcd8ca77f23986df2247bb3f1fe5fd03839e89e143cdbf
X-School-Id: 1
{"id":"0190c2e4-1f00-7000-8000-000000000001","resource":"user","timestamp":"2024-04-15 14:25:32","events":[{"op":"created","pks":[1]}]}
注:上述示例中,Body 顶层的 `timestamp`(字符串)会在签名时覆盖 Header 的 `X-Timestamp`(1713162332,Unix 时间戳)。
校验签名方式如下:
PHP 版本示例
<?php
$token = '87892dedaf483eeabed6c54e4335fbe5';
$nonce = $_SERVER['HTTP_X_NONCE'];
$timestamp = (int)$_SERVER['HTTP_X_TIMESTAMP'];
$signature = $_SERVER['HTTP_X_SIGNATURE'];
$rawBody = file_get_contents('php://input');
$params = [
'nonce' => $nonce,
'timestamp' => $timestamp,
];
$body = json_decode($rawBody, true);
// Body 中的同名 key 会覆盖 Header 的 key(如 timestamp)
$merged = array_merge($params, $body);
// 递归 ksort
function ksort_recursive(array &$arr): void {
ksort($arr);
foreach ($arr as &$v) {
if (is_array($v)) ksort_recursive($v);
}
}
ksort_recursive($merged);
$json = json_encode($merged, JSON_UNESCAPED_UNICODE);
$hash = hash_hmac('sha256', $json, $token);
if ($hash === $signature) {
echo '验证成功';
} else {
echo '验证失败';
}
Python 版本示例
import hashlib
import hmac
import json
token = '87892dedaf483eeabed6c54e4335fbe5'
# 从请求 Header 中获取
nonce = 'bfcf312b'
timestamp = 1713162332
signature = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
# 从请求 Body 中读取并解析
raw_body = '{"id":"0190c2e4-1f00-7000-8000-000000000001","resource":"user","timestamp":"2024-04-15 14:25:32","events":[{"op":"created","pks":[1]}]}'
body = json.loads(raw_body)
params = {
'nonce': nonce,
'timestamp': timestamp,
}
merged = {**params, **body}
def ksort_recursive(obj):
if isinstance(obj, dict):
return {k: ksort_recursive(v) for k, v in sorted(obj.items())}
if isinstance(obj, list):
return [ksort_recursive(i) for i in obj]
return obj
sorted_data = ksort_recursive(merged)
json_data = json.dumps(sorted_data, ensure_ascii=False, separators=(',', ':'))
hash_value = hmac.new(token.encode(), json_data.encode(), hashlib.sha256).hexdigest()
if hash_value == signature:
print('验证成功')
else:
print('验证失败')
响应
推送请求的响应应返回 2xx 状态码(通常为 200),响应 Body 不作要求。