使用 ClickHouse 作为 webhook 终结点并支持 HMAC 验证

图片

本文字数:6128;估计阅读时间:16 分钟

作者:Mark Needham

本文在公众号【ClickHouseInc】首发

图片

ClickHouse 25.12 我最喜欢的功能之一是用于使用共享密钥(shared key)进行消息认证(message authentication)的 HMAC 函数。

ClickHouse 一直能够充当网络钩子端点(webhook endpoint),但有了 HMAC 函数,我们现在可以验证网络钩子请求,过滤掉那些并非来自预期来源的请求。我们将首先通过一个本地示例了解其工作原理,然后再探讨一个使用 ClickHouse Cloud 作为 GitHub 网络钩子端点的真实案例。

网络钩子模式(Webhook Pattern)

ClickHouse 的一个出色之处在于能够使用物化视图(materialized views)将表链接起来。数据被摄入(ingested)到一张表中,物化视图充当 SQL 触发器(SQL trigger),然后结果被写入另一张表。

当与 Null 表引擎(Null table engine)结合使用时,这变得尤其强大。Null 表引擎不存储数据,而是将其接收到的所有内容转发给任何已连接的物化视图——使其成为完美的暂存层(staging layer)。

结果是一个清晰的三步模式:

1. 暂存表接收所有传入数据

2. 物化视图验证并转换数据

3. 最终表仅存储已验证数据

Webhook Diagram Issue 1399.jpg

创建表

让我们来看看如何在 ClickHouse 中进行所有这些设置。如果您想跟着操作,需要运行一个 ClickHouse 服务器或 ClickHouse Cloud 服务。

首先,让我们创建暂存表:

CREATE TABLE webhook_staging (
    received_at DateTime DEFAULT now(),
    raw_payload String,
    signature String DEFAULT getClientHTTPHeader('X-Hub-Signature-256')
) ENGINE = MergeTree()
ORDER BY received_at
SETTINGS allow_get_client_http_header=1;

allow_get_client_http_header 设置是必需的,因为默认情况下禁用了读取客户端请求头(client headers)。

我们正在使用 MergeTree 引擎创建 webhook_staging 表,以便调试传入的请求。在生产系统中,我们将使用 Null 引擎,并且可能还会有一个第二个物化视图来捕获无效请求。

接下来,我们有一个表,它将只存储已验证的行:

CREATE TABLE webhook_logs (
    received_at DateTime,
    payload JSON
) ENGINE = MergeTree()
ORDER BY received_at;

最后,我们有一个物化视图来验证数据:

CREATE MATERIALIZED VIEW webhook_validator TO webhook_logs AS
SELECT
    received_at,
    raw_payload::JSON as payload
FROM webhook_staging
WHERE signature = 'sha256=' || lower(hex(HMAC('SHA256', raw_payload, 'my_secret_key')));

我们正在将请求头(request header)中的签名与使用共享密钥计算出的预期签名进行比较。我们的共享密钥是 my_secret_key

如果传入的行不匹配预期签名,它将不会被写入 webhook_logs 表。

创建受限用户

在我们自己的机器上进行实验时,我们可以使用默认的管理员用户,但对于生产系统而言,创建一个具有最小权限的专用用户更安全。我们创建的用户将用于稍后构建的 webhook URL。

首先,让我们创建用户:

CREATE USER webhook_receiver
IDENTIFIED WITH sha256_hash
BY 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';

该哈希值 (hash) 是一个空字符串的 SHA256,因此当我们构建 webhook URL 时无需密码。

接下来,让我们授予用户所需的权限:

GRANT INSERT ON default.webhook_staging TO webhook_receiver;
GRANT SELECT ON default.webhook_staging TO webhook_receiver;
GRANT SHOW TABLES ON default.webhook_staging TO webhook_receiver;
GRANT SHOW DATABASES ON default.* TO webhook_receiver;

如果我们想格外谨慎,我们还可以对此用户应用速率限制 (rate limits),以防止滥用。

构建 webhook URL

ClickHouse 接受通过 HTTP 进行插入,这意味着整个 webhook 终结点 (endpoint) 是一个嵌入了 INSERT 查询的 URL。

http://localhost:8123/?user=webhook_receiver&query=INSERT+INTO+webhook_staging+(raw_payload)+FORMAT+RawBLOB

关键部分:

• user=webhook_receiver - 我们创建的受限用户

• query=INSERT INTO webhook_staging (raw_payload) FORMAT RawBLOB - 插入原始请求正文 (raw request body) 的 URL 编码查询。

FORMAT RawBLOB 告诉 ClickHouse 将整个请求正文视为一个单一的字符串值,而不是尝试解析它。

然后我们可以通过发送一个有效请求来测试它,即签名是使用正确密钥计算的请求:

PAYLOAD='{"event":"user_login","user_id":456}'
SIGNATURE=$(echo -n ",[object Object]," | openssl dgst -sha256 -hmac "my_secret_key" | cut -d' ' -f2)


curl -X POST "http://localhost:8123/?user=webhook_receiver&allow_get_client_http_header=1&query=INSERT%20INTO%20webhook_staging%20(raw_payload)%20FORMAT%20RawBLOB" \
  -H "X-Hub-Signature-256: sha256=,[object Object]," \
  -d ",[object Object],"

现在让我们使用 ClickHouse 客户端 (Client) 连接到我们的 ClickHouse 服务器 (Server):

clienthouse client

我们可以返回 webhook_staging 和 webhook_logs 的内容:

SELECT * FROM webhook_staging;
Row 1:
──────
received_at: 2026-02-20 15:42:22
raw_payload: {"event":"user_login","user_id":456}
signature:   sha256=5a23c796b6248c725a6ec7fc2cf0788117d69d376ee6241f411a8887297d3ca4


1 row in set. Elapsed: 0.001 sec.
SELECT * FROM webhook_logs;
Row 1:
──────
received_at: 2026-02-20 15:42:22
payload:     {
    "event": "user_login",
    "user_id": 456
}


1 row in set. Elapsed: 0.003 sec.

两张表都有数据。现在,如果我们发送一个使用不同密钥计算签名的请求又会怎样:

PAYLOAD='{"event":"user_login","user_id":456}'
SIGNATURE=$(echo -n ",[object Object]," | openssl dgst -sha256 -hmac "rogue_key" | cut -d' ' -f2)


curl -X POST "http://localhost:8123/?user=webhook_receiver&allow_get_client_http_header=1&query=INSERT%20INTO%20webhook_staging%20(raw_payload)%20FORMAT%20RawBLOB" \
  -H "X-Hub-Signature-256: sha256=,[object Object]," \
  -d ",[object Object],"

让我们对 webhook_staging 运行查询:

Row 1:
──────
received_at: 2026-02-20 15:43:21
raw_payload: {"event":"user_login","user_id":456}
signature:   sha256=01f65041f2505f2b245f3caff410913a22a0ff8c7e8414c9fe1e861359973a7a


Row 2:
──────
received_at: 2026-02-20 15:42:22
raw_payload: {"event":"user_login","user_id":456}
signature:   sha256=5a23c796b6248c725a6ec7fc2cf0788117d69d376ee6241f411a8887297d3ca4


2 rows in set. Elapsed: 0.002 sec.

那么现在来看看 webhook_logs

Row 1:
──────
received_at: 2026-02-20 15:42:22
payload:     {
    "event": "user_login",
    "user_id": 456
}


1 row in set. Elapsed: 0.004 sec.

webhook_staging 得到一个新行,而 webhook_logs 没有——签名不匹配,因此物化视图 (materialized view) 丢弃了它。

GitHub webhook

在本地建立模式后,让我们使用 ClickHouse Cloud 将其连接到一个真实的 GitHub 仓库 (repository)。结构几乎相同,只有一个区别——暂存表 (staging table) 会额外增加一个 event_type 列(来自 GitHub 发送的 X-GitHub-Event 头部 (header)),并且 webhook_logs 也增加一个 event_type 列。

下面的查询创建了我们的表和物化视图。

CREATE TABLE webhook_staging (
    received_at DateTime DEFAULT now(),
    raw_payload String,
    event_type String DEFAULT getClientHTTPHeader('X-GitHub-Event'),
    signature String DEFAULT getClientHTTPHeader('X-Hub-Signature-256')
) ENGINE = MergeTree()
ORDER BY received_at
SETTINGS allow_get_client_http_header=1;
CREATE TABLE webhook_logs (
    received_at DateTime,
    event_type String,
    payload JSON
) ENGINE = MergeTree()
ORDER BY received_at;
CREATE MATERIALIZED VIEW webhook_validator TO webhook_logs AS
SELECT
    received_at,
    event_type,
    raw_payload::JSON as payload
FROM webhook_staging
WHERE signature = 'sha256=' || lower(hex(HMAC('SHA256', raw_payload, 'my_secret_key')));

在 ClickHouse Cloud 中,点击 Connect 获取你的集群 URL,然后以相同的方式构建 webhook URL——只需将 localhost:8123 替换为你的 Cloud 主机,并添加 allow_get_client_http_header=1

https://<host>:8443/?user=webhook_receiver&allow_get_client_http_header=1&query=INSERT+INTO+webhook_staging+(raw_payload)+FORMAT+RawBLOB

配置 GitHub webhook

在你的GitHub仓库中,前往 设置 → Webhooks → Add webhook

1. 将你的ClickHouse URL粘贴到 Payload URL 中

2. 将 Content type 设置为 application/json

3. 在 Secret 字段中输入你的密钥 - 这必须与物化视图(materialized view)中的 my_secret_key 匹配

4. 选择要发送的事件(初步选择推送事件(push events)即可)

5. 点击 Add webhook

2026-02-20_15-48-01.png

GitHub将立即发送一个 ping 事件。刷新webhook页面,你应该会看到“上次交付成功”。

2026-02-20_15-48-31.png

验证功能是否正常

随后,我们可以通过点击表的名称来查看其内容。

你应该在这两个表中都看到 ping 事件,这证实了 HMAC 验证已通过。现在对你的仓库进行更改——例如,提交到一个分支,打开一个拉取请求(PR),然后合并它——之后,推送事件将同时出现在这两个表中。

2026-02-20_16-11-08.png

测试签名验证

为了确认验证功能正常,请回到GitHub,将webhook密钥更改为不同的值,然后进行另一次提交。

webhook_staging 将会有新的行,但 webhook_logs 不会——物化视图计算出了不同的 HMAC 并将其过滤掉了。

Webhook Diagram for Blog.jpg

征稿启示

面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:Tracy.Wang@clickhouse.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值