所有文章
在 localhost 安全测试 Linear webhook
Linearwebhookslocalhostdeveloper integrations

在 localhost 安全测试 Linear webhook

远程服务无法直接访问 localhost。隧道提供公网 HTTPS URL,并把真实 headers 和未经修改的 body 转发到本地服务。 必须在 JSON parser 之前保留 raw body。先验证签名和输入,再持久化投递记录,最后执行后续业务逻辑。

Linear webhook 如何到达本地应用

远程服务无法直接访问 localhost。隧道提供公网 HTTPS URL,并把真实 headers 和未经修改的 body 转发到本地服务。 >官方文档

1. 创建保留 raw body 的 endpoint

必须在 JSON parser 之前保留 raw body。先验证签名和输入,再持久化投递记录,最后执行后续业务逻辑。

import express from "express";
import crypto from "node:crypto";

const app = express();
const secret = process.env.LINEAR_WEBHOOK_SECRET;

app.post(
  "/webhooks/linear",
  express.raw({ type: "application/json", limit: "1mb" }),
  async (req, res) => {
    const rawBody = req.body;
    const signature = req.get("linear-signature");

    if (!verifyLinearSignature(signature, rawBody, secret)) {
      return res.sendStatus(401);
    }

    let payload;
    try {
      payload = JSON.parse(rawBody.toString("utf8"));
    } catch {
      return res.sendStatus(400);
    }

    if (!Number.isFinite(payload.webhookTimestamp) ||
        Math.abs(Date.now() - payload.webhookTimestamp) > 60_000) {
      return res.sendStatus(401);
    }

    const deliveryId = req.get("linear-delivery") || payload.webhookId;
    if (!deliveryId) return res.sendStatus(400);

    try {
      await recordAndEnqueueOnce(deliveryId, payload);
      return res.sendStatus(200);
    } catch (error) {
      console.error("Linear webhook persistence failed", error);
      return res.sendStatus(500);
    }
  }
);

app.use(express.json());
app.listen(3000);

必须在 JSON parser 之前保留 raw body。先验证签名和输入,再持久化投递记录,最后执行后续业务逻辑。 >官方文档

2. 通过 HTTPS 暴露本地端口

启动本地服务后,在另一个终端保持隧道运行。未签名请求返回 401,说明路由正常且鉴权按预期拒绝请求。

npx portpreview 3000
https://example.portpreview.dev/webhooks/linear

启动本地服务后,在另一个终端保持隧道运行。未签名请求返回 401,说明路由正常且鉴权按预期拒绝请求。

3. 在 Linear 中配置 webhook

填写完整 HTTPS URL,只订阅需要的事件,并在测试环境触发真实操作。secret 应保存在代码仓库之外。

填写完整 HTTPS URL,只订阅需要的事件,并在测试环境触发真实操作。secret 应保存在代码仓库之外。

使用原始字节验证签名

按供应商规范使用 webhook secret 和精确原始数据计算 HMAC,采用 constant-time 比较,禁止记录 secret。

function verifyLinearSignature(signature, rawBody, secret) {
  if (!secret || typeof signature !== "string" ||
      !/^[0-9a-f]{64}$/i.test(signature)) {
    return false;
  }

  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest();
  const actual = Buffer.from(signature, "hex");

  return actual.length === expected.length &&
    crypto.timingSafeEqual(actual, expected);
}

按供应商规范使用 webhook secret 和精确原始数据计算 HMAC,采用 constant-time 比较,禁止记录 secret。 >实用指南

基于 timestamp 防止重放

签名通过后检查 timestamp 是否新鲜,并保持主机时间同步,避免误拒绝合法投递。

签名通过后检查 timestamp 是否新鲜,并保持主机时间同步,避免误拒绝合法投递。

理解 payload 和投递标识

只处理已知 type,兼容新增字段,并用稳定的投递 ID 在持久化存储中去重。

只处理已知 type,兼容新增字段,并用稳定的投递 ID 在持久化存储中去重。

快速响应并保证幂等处理

在同一 transaction 中预留投递 ID 并创建 job,然后返回 200。持久化不可用时应返回错误等待重试。

在同一 transaction 中预留投递 ID 并创建 job,然后返回 200。持久化不可用时应返回错误等待重试。 >实用指南

测试完整的本地链路

  1. 启动本地服务后,在另一个终端保持隧道运行。未签名请求返回 401,说明路由正常且鉴权按预期拒绝请求。
  2. 填写完整 HTTPS URL,只订阅需要的事件,并在测试环境触发真实操作。secret 应保存在代码仓库之外。
  3. 按供应商规范使用 webhook secret 和精确原始数据计算 HMAC,采用 constant-time 比较,禁止记录 secret。
  4. 在同一 transaction 中预留投递 ID 并创建 job,然后返回 200。持久化不可用时应返回错误等待重试。

依次验证路由、headers、签名、响应时限和幂等性。模拟请求更适合测试拒绝分支。

排查 Linear webhook 投递问题

  • 启动本地服务后,在另一个终端保持隧道运行。未签名请求返回 401,说明路由正常且鉴权按预期拒绝请求。
  • 按供应商规范使用 webhook secret 和精确原始数据计算 HMAC,采用 constant-time 比较,禁止记录 secret。
  • 签名通过后检查 timestamp 是否新鲜,并保持主机时间同步,避免误拒绝合法投递。
  • 重点检查 POST path、隧道 port、raw body、secret、系统时间和数据库延迟。

>实用指南

本地与生产环境安全清单

  • 强制使用 HTTPS,限制请求大小与 method,保护 secret,脱敏日志,并删除过期测试 URL。
  • 必须在 JSON parser 之前保留 raw body。先验证签名和输入,再持久化投递记录,最后执行后续业务逻辑。
  • 在同一 transaction 中预留投递 ID 并创建 job,然后返回 200。持久化不可用时应返回错误等待重试。

强制使用 HTTPS,限制请求大小与 method,保护 secret,脱敏日志,并删除过期测试 URL。 >实用指南

常见问题

如何在 localhost 测试 Linear webhook?
启动本地服务后,在另一个终端保持隧道运行。未签名请求返回 401,说明路由正常且鉴权按预期拒绝请求。
如何验证 Linear webhook 签名?
按供应商规范使用 webhook secret 和精确原始数据计算 HMAC,采用 constant-time 比较,禁止记录 secret。
Linear 为什么会重试 webhook?
在同一 transaction 中预留投递 ID 并创建 job,然后返回 200。持久化不可用时应返回错误等待重试。
幂等处理应该使用哪个键?
只处理已知 type,兼容新增字段,并用稳定的投递 ID 在持久化存储中去重。