飞书 Webhook 集成实战:机器人推送、事件订阅与消息卡片(2025 完整指南)

飞书 Webhook 集成完整指南:群机器人推送、自定义机器人回调、应用事件订阅(Encrypt Key 解密)、消息卡片构建。含 Go/Node.js 签名验证与解密代码。

TL;DR:飞书(Lark)Webhook 机制与钉钉类似,但更清晰统一——自定义机器人(群推送 + 回调)、应用事件订阅(企业级通知)。核心差异在于飞书使用 Encrypt Key 对回调 Payload 进行 AES-256-CBC 加密,且消息卡片(Card)生态更强大。


1. 飞书 Webhook 类型速览

类型方向触发场景验证方式适用
自定义机器人你 → 群 / 群 → 你推送消息 / 用户@机器人无 / Encrypt Key + sign通知、交互
应用事件订阅飞书 → 你审批/日程/通讯录变更Encrypt Key + Verification Token企业数据同步

2. 自定义机器人(群推送)

2.1 配置步骤

  1. 进入飞书群 → 群设置 → 群机器人添加机器人自定义机器人
  2. 设置名称、描述、头像
  3. 获得 Webhook URL
https://open.feishu.cn/open-apis/bot/v2/hook/xxx
  1. 可选启用签名校验(否则任何人知道 URL 就能推送)

2.2 安全设置(推荐启用)

  • 签名校验:使用 secret 生成 HMAC-SHA256 + timestamp
  • IP 白名单:仅指定 IP 段可调
  • 关键词:消息中必须包含指定关键词(与钉钉相同)

2.3 Go 推送代码

package main

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

type FeishuRobot struct {
    WebhookURL string
    Secret     string // 签名校验密钥
}

func (r *FeishuRobot) SendText(text string) error {
    timestamp := time.Now().Unix()
    sign := r.sign(timestamp)

    payload := map[string]interface{}{
        "timestamp": timestamp,
        "sign":      sign,
        "msg_type":  "text",
        "content": map[string]string{
            "text": text,
        },
    }

    body, _ := json.Marshal(payload)
    resp, err := http.Post(r.WebhookURL, "application/json", bytes.NewReader(body))
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    var result struct {
        Code int    `json:"code"`
        Msg  string `json:"msg"`
    }
    json.NewDecoder(resp.Body).Decode(&result)

    if result.Code != 0 {
        return fmt.Errorf("feishu robot send failed: %s", result.Msg)
    }
    return nil
}

func (r *FeishuRobot) sign(timestamp int64) string {
    // 签名字串:timestamp + "\n" + secret
    strToSign := fmt.Sprintf("%d\n%s", timestamp, r.Secret)
    mac := hmac.New(sha256.New, []byte(r.Secret))
    mac.Write([]byte(strToSign))
    return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}

// 使用示例
func main() {
    robot := &FeishuRobot{
        WebhookURL: "https://open.feishu.cn/open-apis/bot/v2/hook/xxx",
        Secret:     "xxx",
    }
    robot.SendText("📢 系统告警:API 响应时间 P99 > 2s")
}

2.4 飞书消息卡片(Card)

飞书的卡片消息比钉钉更强大,支持丰富的交互组件:

func (r *FeishuRobot) SendCard(title, content string) error {
    payload := map[string]interface{}{
        "msg_type": "interactive",
        "card": map[string]interface{}{
            "header": map[string]interface{}{
                "title": map[string]interface{}{
                    "tag":     "plain_text",
                    "content": title,
                },
                "template": "red", // red / orange / green / blue
            },
            "elements": []map[string]interface{}{
                {
                    "tag": "div",
                    "text": map[string]interface{}{
                        "tag":     "lark_md",
                        "content": content,
                    },
                },
                {
                    "tag": "action",
                    "actions": []map[string]interface{}{
                        {
                            "tag": "button",
                            "text": map[string]interface{}{
                                "tag":     "plain_text",
                                "content": "查看详情",
                            },
                            "type": "primary",
                            "url":  "https://admin.example.com/alerts/123",
                        },
                    },
                },
            },
        },
    }

    body, _ := json.Marshal(payload)
    resp, _ := http.Post(r.WebhookURL, "application/json", bytes.NewReader(body))
    resp.Body.Close()
    return nil
}

卡片效果预览:

┌─────────────────────────────┐
│ 🔴 系统告警                   │
├─────────────────────────────┤
│ API 响应时间 P99 > 2s       │
│ 影响接口: /api/v1/orders    │
│ 发生时间: 10:30:00          │
├─────────────────────────────┤
│ [查看详情]                   │
└─────────────────────────────┘

3. 自定义机器人回调(用户@机器人)

3.1 配置回调 URL

  1. 飞书开发者后台 → 机器人 → 事件订阅
  2. 配置 请求网址https://yourapp.com/webhooks/feishu
  3. 订阅事件:im.message.receive_v1

3.2 回调 Payload 结构

{
  "schema": "2.0",
  "header": {
    "event_id": "xxx",
    "event_type": "im.message.receive_v1",
    "create_time": "1234567890000",
    "token": "xxx",
    "app_id": "cli_xxx",
    "tenant_key": "xxx"
  },
  "event": {
    "message": {
      "message_id": "om_xxx",
      "message_type": "text",
      "content": "{\"text\":\"@机器人 查询用户\"}",
      "chat_id": "oc_xxx",
      "sender": {"sender_id": {"user_id": "ou_xxx"}, "sender_type": "user"}
    }
  }
}

3.3 Go Handler

func handleFeishuWebhook(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)

    var event struct {
        Header struct {
            EventType string `json:"event_type"`
            Token     string `json:"token"`
        } `json:"header"`
        Event struct {
            Message struct {
                MessageType string `json:"message_type"`
                Content     string `json:"content"` // JSON 字符串,需二次解析
                ChatID      string `json:"chat_id"`
            } `json:"message"`
        } `json:"event"`
    }
    json.Unmarshal(body, &event)

    // 验证 Token
    if event.Header.Token != os.Getenv("FEISHU_VERIFICATION_TOKEN") {
        http.Error(w, "invalid token", http.StatusUnauthorized)
        return
    }

    switch event.Header.EventType {
    case "im.message.receive_v1":
        var content struct {
            Text string `json:"text"`
        }
        json.Unmarshal([]byte(event.Event.Message.Content), &content)

        reply := processFeishuCommand(content.Text)
        sendFeishuReply(event.Event.Message.ChatID, reply)
    }

    w.WriteHeader(http.StatusOK)
}

func processFeishuCommand(text string) string {
    switch {
    case strings.Contains(text, "用户"):
        return "👥 今日新增用户 42 人,日活 3,024"
    case strings.Contains(text, "订单"):
        return "📦 今日订单 128 笔,金额 ¥12,480"
    default:
        return "🤖 可用指令:查询用户 / 查询订单 / 系统状态"
    }
}

func sendFeishuReply(chatID, text string) {
    token := getFeishuTenantToken()
    url := "https://open.feishu.cn/open-apis/im/v1/messages"

    payload := map[string]interface{}{
        "receive_id": chatID,
        "msg_type":   "text",
        "content":    fmt.Sprintf(`{"text":"%s"}`, text),
    }

    body, _ := json.Marshal(payload)
    req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
    req.Header.Set("Authorization", "Bearer "+token)
    req.Header.Set("Content-Type", "application/json")

    http.DefaultClient.Do(req)
}

4. 应用事件订阅(Encrypt Key 解密)

4.1 加密机制

飞书使用 AES-256-CBC 加密回调数据,比钉钉多了一个消息完整性校验步骤:

加密流程:明文 → AES-256-CBC 加密 → Base64 → 放入 encrypt 字段
解密流程:encrypt → Base64 解码 → AES-256-CBC 解密 → 去掉 16 字节随机串 → 明文

4.2 Go 解密实现

import (
    "crypto/aes"
    "crypto/cipher"
    "encoding/base64"
)

func decryptFeishuEvent(encrypt string, encryptKey string) ([]byte, error) {
    // encryptKey 是 32 字节的 Base64 编码字符串 → 解码后 48 字节
    key, err := base64.StdEncoding.DecodeString(encryptKey)
    if err != nil {
        return nil, err
    }

    // 飞书要求 key 为 32 字节
    aesKey := key[:32]

    data, err := base64.StdEncoding.DecodeString(encrypt)
    if err != nil {
        return nil, err
    }

    block, err := aes.NewCipher(aesKey)
    if err != nil {
        return nil, err
    }

    iv := aesKey[:16]
    mode := cipher.NewCBCDecrypter(block, iv)
    mode.CryptBlocks(data, data)

    // PKCS#7 去填充
    padLen := int(data[len(data)-1])
    return data[16 : len(data)-padLen], nil // 去掉 16 字节随机串
}

4.3 事件处理 Handler

func handleFeishuEvent(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)

    var event struct {
        Encrypt string `json:"encrypt"`
        Token   string `json:"token"`   // 挑战验证时携带
        Challenge string `json:"challenge"` // 初始配置 URL 验证
    }
    json.Unmarshal(body, &event)

    // ① URL 验证(首次配置回调地址时)
    if event.Challenge != "" {
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{
            "challenge": event.Challenge,
        })
        return
    }

    // ② 解密事件
    decrypted, err := decryptFeishuEvent(event.Encrypt, os.Getenv("FEISHU_ENCRYPT_KEY"))
    if err != nil {
        http.Error(w, "decrypt failed", http.StatusBadRequest)
        return
    }

    var payload struct {
        Header struct {
            EventType string `json:"event_type"`
        } `json:"header"`
        Event struct {
            ApprovalInstance struct {
                InstanceCode string `json:"instance_code"`
                Status       string `json:"status"` // PENDING / COMPLETED
                Result       string `json:"result"` // agree / refuse
            } `json:"approval_instance"`
        } `json:"event"`
    }
    json.Unmarshal(decrypted, &payload)

    switch payload.Header.EventType {
    case "approval_instance_status_changed":
        handleApprovalChange(payload.Event.ApprovalInstance)
    case "contact.user.deleted_v3":
        handleUserDeleted(payload)
    }

    w.WriteHeader(http.StatusOK)
}

5. 飞书 vs 钉钉对比

维度飞书钉钉
推送 URL 格式open.feishu.cn/open-apis/bot/v2/hook/xxxoapi.dingtalk.com/robot/send?access_token=xxx
加签格式timestamp(秒) + “\n” + secrettimestamp(毫秒) + “\n” + secret
加密算法AES-256-CBC(易实现)AES-CBC(类似)
消息卡片更强大(富交互组件)较简单(支持 Markdown)
机器人回复需 tenant_access_token + chat_idsessionWebhook 直接回复
开放平台 APIOpenAPI 3.0 风格OAPI 传统风格
日期时间格式Unix 秒Unix 毫秒(注意区别!)

⚠️ 最大坑点:飞书回调的时间戳是秒级,钉钉是毫秒级,转换时容易搞混。


6. 常见问题排查

#问题排查修复
1“msg: key not exist”Webhook URL 中 hook 后面的 ID 错误确认机器人配置中的 Webhook URL
2推送成功但群里看不到机器人不在群内 / 被禁言重新邀请机器人到群
3回调 URL 验证失败未正确返回 challengeChallenge 验证必须原样返回
4Decrypt 失败Encrypt Key 不是 32 字节使用 Base64 编码后的字符串,保持 43 字符
5获取 tenant_access_token 失败AppID / AppSecret 错误检查开发者后台的凭证
6时间戳超过 1h系统时间不准配置 NTP,timedatectl set-ntp true

7. 下一步


本文全场约 3,500 词,提供 群机器人推送、自定义机器人回调、应用事件订阅解密的完整 Go 代码,以及 飞书 vs 钉钉对比表消息卡片模板,可直接用于飞书集成开发项目。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「saas」更多文章