You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Golang Fiber中验证Trello Webhook签名?哈希不一致问题排查

Trello Webhook签名验证Golang Fiber实现问题

我参考Trello官方Webhook文档,将Node.js的签名验证逻辑转为Golang Fiber代码,但生成的哈希始终与Trello返回的哈希不一致,尝试了三种实现均失败,求指出遗漏点。


尝试代码1

func TrelloWebhook(c *fiber.Ctx) error {
    var (
        TRELLO_SECRET = "****ea6308995"
        webhookURL    = "https://*****ssum.ngrok-free.app/trello-webhook"
    )

    base64Digest := func(secret string) string {
        key := []byte(TRELLO_SECRET)
        mac := hmac.New(sha1.New, key)
        mac.Write([]byte(secret))
        return base64.StdEncoding.EncodeToString(mac.Sum(nil))
    }

    hashedHeader := c.Get("x-trello-webhook")
    requestBody := string(c.BodyRaw())

    doubleHashedHeader := base64Digest(hashedHeader)
    doubleHashedBody := base64Digest(base64Digest(requestBody + webhookURL))

    fmt.Println("doubleHashedHeader : ", doubleHashedHeader)
    fmt.Println("doubleHashedBody : ", doubleHashedBody)

    if hmac.Equal([]byte(doubleHashedHeader), []byte(doubleHashedBody)) {
        fmt.Println("SUCCESS")
    }
    // Your existing return statement
    return c.Status(200).JSON(&fiber.Map{
        "status":      true,
        "information": "customers-data_request!",
    })
}

尝试代码2

func TrelloWebhook(c *fiber.Ctx) error {

    secret := "****ea6308995"
    callbackURL := "https://*****ssum.ngrok-free.app/trello-webhook"

    base64Digest := func(s string) string {
        h := hmac.New(sha1.New, []byte(secret))
        h.Write([]byte(s))
        return base64.StdEncoding.EncodeToString(h.Sum(nil))
    }

    var request map[string]interface{}
    if err := json.Unmarshal(c.Body(), &request); err != nil {
        fmt.Println("Error decoding JSON:", err)
        return c.SendStatus(fiber.StatusBadRequest)
    }

    contentBuffer := bytes.NewBuffer(c.Body())
    contentBuffer.WriteString(callbackURL)
    content := contentBuffer.String()

    doubleHash := base64Digest(content)
    headerHash := c.Get("x-trello-webhook")

    if doubleHash == headerHash {
        fmt.Println(" Working ? ")
    }

    fmt.Println("doubleHash : ", doubleHash)
    fmt.Println("headerHash : ", headerHash)

    // Your existing return statement
    return c.Status(200).JSON(&fiber.Map{
        "status":      true,
        "information": "customers-data_request!",
    })
}

尝试代码3

func TrelloWebhook(c *fiber.Ctx) error {

    secret := "****ea6308995"
    callbackURL := "https://*****ssum.ngrok-free.app/trello-webhook"

    base64Digest := func(s string) string {
        h := hmac.New(sha1.New, []byte(secret))
        h.Write([]byte(s))
        return base64.StdEncoding.EncodeToString(h.Sum(nil))
    }

    var request map[string]interface{}
    if err := json.Unmarshal(c.Body(), &request); err != nil {
        fmt.Println("Error decoding JSON:", err)
        return c.SendStatus(fiber.StatusBadRequest)
    }

    requestBody, err := json.Marshal(request)
    if err != nil {
        fmt.Println("Error encoding JSON:", err)
        return c.SendStatus(fiber.StatusInternalServerError)
    }

    content := string(requestBody) + callbackURL

    doubleHash := base64Digest(content)
    headerHash := c.Get("x-trello-webhook")

    fmt.Println("doubleHash : ", doubleHash)
    fmt.Println("headerHash : ", headerHash)

    // Your existing return statement
    return c.Status(200).JSON(&fiber.Map{
        "status":      true,
        "information": "customers-data_request!",
    })
}

问题分析与正确实现

核心错误点

  1. 错误的哈希逻辑:尝试1中对结果进行了双重哈希,完全不符合Trello的签名规则(Trello只做一次HMAC-SHA1+Base64编码)
  2. Body被修改:尝试3中将原始Body解析后重新Marshal,导致JSON格式(空格、键顺序、缩进)与Trello发送的原始Body不一致,拼接后的字符串无法匹配签名生成的源数据
  3. 回调URL一致性问题:若代码中的回调URL与Trello后台配置的不一致(如大小写、末尾斜杠、HTTP/HTTPS),会直接导致哈希不匹配

正确实现代码

import (
    "crypto/hmac"
    "crypto/sha1"
    "encoding/base64"
    "fmt"

    "github.com/gofiber/fiber/v2"
)

func TrelloWebhook(c *fiber.Ctx) error {
    const (
        trelloSecret = "你的Trello Secret密钥"
        callbackURL  = "与Trello后台完全一致的回调URL" // 注意大小写、末尾斜杠、协议必须完全匹配
    )

    // 生成Trello格式的签名
    generateSignature := func(data string) string {
        h := hmac.New(sha1.New, []byte(trelloSecret))
        _, _ = h.Write([]byte(data)) // 写入字节流无失败可能,忽略错误
        return base64.StdEncoding.EncodeToString(h.Sum(nil))
    }

    // 获取原始请求Body(必须用BodyRaw保证未被修改)
    rawBody := string(c.BodyRaw())
    // 拼接原始Body与回调URL(顺序不可调换)
    signatureSource := rawBody + callbackURL
    // 计算本地签名
    computedSignature := generateSignature(signatureSource)
    // 获取Trello发送的签名
    trelloSignature := c.Get("x-trello-webhook")

    // 用hmac.Equal避免时序攻击,安全比较签名
    if hmac.Equal([]byte(computedSignature), []byte(trelloSignature)) {
        fmt.Println("签名验证成功")
        return c.Status(fiber.StatusOK).JSON(fiber.Map{
            "status": true,
            "msg":    "Webhook请求验证通过",
        })
    }

    fmt.Println("签名验证失败")
    fmt.Println("本地计算签名:", computedSignature)
    fmt.Println("Trello发送签名:", trelloSignature)
    return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
        "status": false,
        "msg":    "签名验证失败",
    })
}

额外注意事项

  • 回调URL严格匹配:必须与Trello后台配置的Webhook回调URL完全一致,包括协议(HTTP/HTTPS)、域名、路径、大小写、末尾是否带斜杠
  • 避免修改原始Body:不要对请求Body做解析后重新序列化的操作,必须使用原始的请求字节
  • 哈希算法正确性:Trello使用HMAC-SHA1算法,不要误用SHA256等其他算法
  • 安全比较签名:始终使用hmac.Equal而非直接字符串比较,防止时序攻击

内容的提问来源于stack exchange,提问作者javmah

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 21:54:59