要在本地主机上测试 GitLab webhook,请通过 HTTPS 隧道暴露您的本地处理程序,将该 URL 添加到 设置 → Webhooks 下,生成签名令牌,并在解析有效负载之前验证 GitLab 的标准 Webhooks 签名。 触发一次 push 或 merge request,检查传递内容,并在每次更改后无需部署您的集成即可在本地迭代。
使用 GitLab 签名令牌,而不是新的明文秘密令牌。
GitLab 支持两种容易混淆的机制。较旧的秘密令牌会被复制到 X-Gitlab-Token 请求头中。它证明了共享值的知识,但不能保护主体的完整性。GitLab 现在建议使用 签名令牌 进行新的 webhook。它会生成 HMAC-SHA256 签名,并遵循标准 Webhooks 消息格式。
官方 GitLab Webhook 文档 表示已签名的请求包含 webhook-id, webhook-timestamp,和 webhook-signature签名涵盖消息 ID、时间戳和完整的原始 JSON 内容。这可以保护来源和负载的完整性。
在 Node.js 中实现标准 Webhooks 验证
GitLab 签名令牌只显示一次并使用 whsec_ 前缀。移除该前缀并对剩余部分进行 Base64 解码以获得 HMAC 密钥。每个收到的签名的形式为 v1,<base64 signature>; 头部可能包含几个以空格分隔的签名。
import crypto from 'node:crypto';
function safeEqual(a, b) {
const left = Buffer.from(a);
const right = Buffer.from(b);
return left.length === right.length &&
crypto.timingSafeEqual(left, right);
}
function verifyGitLabWebhook({ token, id, timestamp, body, signatures }) {
if (!token?.startsWith('whsec_') || !id || !timestamp || !signatures) {
return false;
}
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > 300) return false;
const key = Buffer.from(token.slice(6), 'base64');
const message = `${id}.${timestamp}.${body}`;
const digest = crypto.createHmac('sha256', key).update(message).digest('base64');
const expected = `v1,${digest}`;
return signatures.split(' ').some((value) => safeEqual(value, expected));
}
这里显示的五分钟时间戳窗口是一种应用策略,而不是盲目复制的数值。选择一个能够容纳时钟偏差但阻止有效重放的容差。同步接收机器的时钟。存储每个被接受的 webhook-id 由于仅检查一个新的时间戳无法防止同一消息的两次立即传递,因此需要在唯一约束下进行。
构建 Express webhook 路由
在此路由上捕获原始主体。在验证之前的全局 express.json() 调用会破坏 GitLab 签名的逐字节表示。
import express from 'express';
const app = express();
app.post(
'/webhooks/gitlab',
express.raw({ type: 'application/json', limit: '2mb' }),
async (req, res) => {
const body = req.body.toString('utf8');
const valid = verifyGitLabWebhook({
token: process.env.GITLAB_WEBHOOK_SIGNING_TOKEN,
id: req.get('webhook-id'),
timestamp: req.get('webhook-timestamp'),
signatures: req.get('webhook-signature'),
body,
});
if (!valid) return res.sendStatus(401);
await inbox.insertOnce(req.get('webhook-id'), JSON.parse(body));
return res.sendStatus(202);
},
);
app.use(express.json());
app.listen(3000);
在 webhook 路由之后挂载普通 JSON 解析器,或使用其 verify 回调来保留原始缓冲区。绝不要仅因为端点转发到本地主机就禁用签名检查;隧道 URL 从公共互联网仍然可访问。
创建公共 HTTPS 端点
- 在本地启动集成,并使用故意未签名的请求测试其路由。它应返回 401,证明身份验证已启用。
- 在另一个终端运行
npx portpreview 3000。 - 复制 HTTPS 源并附加
/webhooks/gitlab. - 在整个配置和事件测试期间保持隧道开放。
GitLab 的 SSL 验证应保持启用。使用公开信任的 TLS 的隧道可以避免自签名证书错误。如果 GitLab 运行在私有自管网络中,它还必须能够访问公共隧道 URL。
配置项目 Webhook
- 打开 GitLab 项目并选择 设置 → 网络钩子.
- 选择 添加新的网络钩子 并粘贴完整的隧道传输网址。
- 选择 生成签名令牌,立即复制令牌,并将其保存在
GITLAB_WEBHOOK_SIGNING_TOKEN. - 仅选择必需的触发器——例如推送事件、合并请求事件、标签推送事件或流水线事件。
- 保持启用 SSL 验证并保存 webhook。
- 使用 GitLab 的测试操作或生成真实事件,然后检查本地请求和 GitLab 交付历史。
设置环境变量后重启本地进程。如果您正在迁移现有集成,GitLab 允许同时使用签名令牌和旧版秘密令牌。验证 webhook-signature 时,如果存在,可暂时回退到 X-Gitlab-Token,然后在所有接收端支持签名后删除较弱的秘密。
通过头部和负载分发 GitLab 事件
X-Gitlab-Event 会给出一个可读的事件名称,例如 Push Hook 或 Merge Request Hook。可用于路由,但也要验证负载的 object_kind 。这样可以使意外的组合可见。
switch (req.get('x-gitlab-event')) {
case 'Push Hook':
await handlePush(payload);
break;
case 'Merge Request Hook':
await handleMergeRequest(payload);
break;
case 'Pipeline Hook':
await handlePipeline(payload);
break;
default:
await recordUnsupportedGitLabEvent(payload.object_kind);
}
推送事件
测试分支创建、普通提交、强制推送和分支删除。零 SHA 可以表示引用转换中缺失的一端。大型推送可能与一次提交的固定装置不同,所以不要假设每个已更改的提交都会出现在无限数组中。请使用项目和引用标识符,而不是解析显示字符串。
合并请求事件
打开、更新、审批、合并和关闭等操作可能共享相同的广义事件类型。请根据已记录的对象属性进行路由,并确保重复更新具有幂等性。切勿仅因为可变标题或用户名匹配而合并代码或批准部署。
流水线和作业事件
这些事件可能频繁发生。在 GitLab 过滤一次,再在处理程序中按项目、分支、状态和环境过滤。对于耗时的工件或部署工作,请先排队处理并确认 Webhook。
为重试和递归触发设计
GitLab 包含 webhook-id,它在重试过程中保持一致,并等同于传统的 Idempotency-Key。将其用作交付幂等性键。 X-Gitlab-Webhook-UUID 识别一次 webhook 执行,而 X-Gitlab-Event-UUID 可以帮助追踪事件;递归 webhook 可能会共享事件 UUID。
如果处理程序通过 API 修改 GitLab,可能会创建另一个 webhook。添加显式循环防护:用你的集成身份标记操作,忽略不改变期望状态的更改,并限制工作流转换次数。 重试和幂等性指南 涵盖事务收件箱模式。
排查 GitLab webhook 测试失败
GitLab 无法连接到 URL
确认隧道进程正在运行,完整路径正确,并且您的本地服务器正在监听转发端口。对于自管理的 GitLab,请检查出站网络策略和 DNS。不要为了隐藏无关的路由故障而清除 SSL 验证。
签名从未匹配
使用签名令牌,而不是旧的秘密令牌。去掉 whsec_,对剩余的令牌进行 Base64 解码,然后签名 {webhook-id}.{webhook-timestamp}.{raw body}。将二进制 HMAC 摘要进行 Base64 编码,并在前面加上 v1,。与每个空格分隔的签名进行比较。
时间戳被拒绝
检查系统时间和时区处理;该头是以秒为单位的 Unix 时间戳。不要在不除以 1000 的情况下将其与 JavaScript 毫秒比较。如果调试捕获的旧请求,时间戳被拒绝是正确的重放保护。
GitLab 禁用或暂停 webhook
检查最近的投递状态和你的路由响应。在持久接受后快速返回 2xx。重复的 401 表示令牌配置错误;重复的 5xx 表示处理程序失败;超时表示同步工作过多。
只有部分事件到达
检查选定的触发器和分支过滤器。组和项目 webhook 的作用范围不同。确认事件确实发生在配置此 webhook 的项目中。
保持本地 GitLab webhook 数据安全
- 仅将签名令牌存储在忽略的环境文件中,并旋转任何泄露的令牌。
- 在执行副作用之前验证签名、时间戳、项目 ID 和允许的事件类型。
- 从捕获中编辑提交信息、私有仓库 URL、用户邮箱和 CI 变量。
- 仅为集成 API 令牌提供其下游操作所需的权限范围。
- 测试结束后删除本地负载历史记录。
对于与提供者无关的诊断,请使用 本地 webhooks 调试指南。GitHub 使用不同的签名格式,因此请参考单独的 GitHub webhooks 指南 ,而不是重复使用其验证器。
