钉钉 Webhook 集成实战:机器人推送、Outgoing 回调与事件订阅(2025 完整指南)

钉钉 Webhook 集成完整指南:群机器人 Webhook 推送、Outgoing 机器人回调验签、企业内应用事件订阅。含 Go/Node.js 签名验证代码与消息卡片模板。

TL;DR:钉钉提供 3 种 Webhook 机制——群机器人(主动推送消息到群)、Outgoing 机器人(用户@机器人时回调你的服务)、事件订阅(企业内审批/日程等变更通知)。本文给出每种场景的 Go/Node.js 代码和避坑指南。


1. 钉钉 Webhook 类型速览

类型方向触发场景验签方式适用
群机器人你 → 钉钉群服务端主动推送无(只需 access_token)通知类消息
Outgoing 机器人用户 → 你用户@机器人HMAC-SHA256 + timestamp交互式机器人
事件订阅钉钉 → 你企业内数据变更AES-CBC 加密 + 签名审批/日程同步

2. 群机器人 Webhook(最简单)

2.1 配置步骤

  1. 进入目标群 → 群设置 → 智能群助手添加机器人自定义
  2. 设置机器人名称、头像
  3. 获得 Webhook URLaccess_token
https://oapi.dingtalk.com/robot/send?access_token=xxx

2.2 安全设置

三种方式(建议全选):

  • 自定义关键词:Payload 中必须包含指定关键词
  • 加签:使用密钥生成 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 DingTalkRobot struct {
    WebhookURL string
    Secret     string // 加签密钥
}

func (r *DingTalkRobot) SendText(content string) error {
    timestamp := time.Now().UnixMilli()
    sign := r.sign(timestamp)

    url := fmt.Sprintf("%s&timestamp=%d&sign=%s", r.WebhookURL, timestamp, sign)

    payload := map[string]interface{}{
        "msgtype": "text",
        "text": map[string]string{
            "content": content,
        },
    }

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

    if resp.StatusCode != 200 {
        return fmt.Errorf("dingtalk robot send failed: %d", resp.StatusCode)
    }
    return nil
}

func (r *DingTalkRobot) 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 := &DingTalkRobot{
        WebhookURL: "https://oapi.dingtalk.com/robot/send?access_token=xxx",
        Secret:     "SECxxx",
    }
    robot.SendText("🚀 部署成功通知:服务 v1.2.3 已上线")
}

2.4 Markdown 消息与卡片

func (r *DingTalkRobot) SendMarkdown(title, text string) error {
    payload := map[string]interface{}{
        "msgtype": "markdown",
        "markdown": map[string]string{
            "title": title,
            "text":  text,
        },
    }
    // ... 同上发送逻辑
    return nil
}

Markdown 消息示例:

## 订单支付通知
- **订单号**:202501150001
- **金额**:¥299.00
- **状态**:✅ 支付成功
- **时间**:2025-01-15 10:30:00

[查看详情](https://admin.example.com/orders/202501150001)

3. Outgoing 机器人回调

用户@机器人时,钉钉向你的回调 URL 发送 HTTP POST。

3.1 回调 Payload 结构

{
  "conversationType": "1",
  "atUsers": [{"dingtalkId": "$:LWCP_v1:$xxx"}],
  "chatbotCorpId": "dingxxx",
  "chatbotUserId": "$:LWCP_v1:$xxx",
  "msgId": "msgxxx",
  "senderStaffId": "userxxx",
  "senderNick": "张三",
  "senderCorpId": "dingxxx",
  "sessionWebhook": "https://oapi.dingtalk.com/robot/sendBySession?session=xxx",
  "text": {"content": " @机器人 查询订单"},
  "msgtype": "text",
  "createAt": 1705302000000
}

3.2 回调验签(HMAC-SHA256 + Base64)

钉钉回调 Header 中携带签名:

  • timestamp:请求发送时间戳
  • sign:HMAC-SHA256 签名
func verifyDingTalkCallback(body []byte, timestamp, signature, secret string) error {
    strToSign := timestamp + "\n" + secret
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(strToSign))
    expectedSign := base64.StdEncoding.EncodeToString(mac.Sum(nil))

    if signature != expectedSign {
        return fmt.Errorf("sign mismatch")
    }

    // 防重放:时间戳必须在 1 小时内
    ts, _ := strconv.ParseInt(timestamp, 10, 64)
    if time.Since(time.UnixMilli(ts)) > time.Hour {
        return fmt.Errorf("timestamp too old")
    }

    return nil
}

3.3 Go HTTP Handler

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

    timestamp := r.Header.Get("timestamp")
    sign := r.Header.Get("sign")

    if err := verifyDingTalkCallback(body, timestamp, sign, os.Getenv("DINGTALK_SECRET")); err != nil {
        http.Error(w, err.Error(), http.StatusUnauthorized)
        return
    }

    var msg struct {
        MsgType string `json:"msgtype"`
        Text    struct {
            Content string `json:"content"`
        } `json:"text"`
        SessionWebhook string `json:"sessionWebhook"`
    }
    json.Unmarshal(body, &msg)

    // 处理用户指令
    reply := processCommand(strings.TrimSpace(msg.Text.Content))

    // 通过 sessionWebhook 回复(无需 access_token)
    sendReply(msg.SessionWebhook, reply)

    w.WriteHeader(http.StatusOK)
}

func processCommand(cmd string) string {
    switch {
    case strings.Contains(cmd, "订单"):
        return "📦 今日订单:15 笔,总金额 ¥4,230"
    case strings.Contains(cmd, "用户"):
        return "👥 今日新增:23 人,活跃用户 1,024"
    default:
        return "🤖 可用指令:查询订单 / 查询用户 / 系统状态"
    }
}

func sendReply(sessionWebhook, text string) {
    payload := map[string]interface{}{
        "msgtype": "text",
        "text":    map[string]string{"content": text},
    }
    body, _ := json.Marshal(payload)
    http.Post(sessionWebhook, "application/json", bytes.NewReader(body))
}

4. 事件订阅(企业应用)

用于接收审批状态变更、日程变动、通讯录变更等企业级事件。

4.1 事件订阅配置

  1. 登录 钉钉开放平台
  2. 进入应用 → 事件与回调事件订阅
  3. 配置加密 aes_key(43 字符 Base64)和 签名 token
  4. 订阅需要的事件类型(如 bpms_instance_changeattendance_check_record

4.2 数据加密机制

钉钉使用 AES-CBC 加密回调数据:

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

func decryptDingTalkEvent(encrypt string, aesKey string) ([]byte, error) {
    // 钉钉加密流程:Base64 → AES-256-CBC 解密 → PKCS#7 去填充 → 去掉随机串
    key, _ := base64.StdEncoding.DecodeString(aesKey)
    data, _ := base64.StdEncoding.DecodeString(encrypt)

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

    iv := key[: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 handleDingTalkEvent(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)

    var event struct {
        Encrypt string `json:"encrypt"`
    }
    json.Unmarshal(body, &event)

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

    var payload struct {
        EventType   string          `json:"EventType"`
        ProcessCode string          `json:"processCode"`
        ProcessInstance struct {
            Title  string `json:"title"`
            Result string `json:"result"` // agree / refuse
            Status string `json:"status"` // RUNNING / COMPLETED
        } `json:"processInstance"`
    }
    json.Unmarshal(decrypted, &payload)

    switch payload.EventType {
    case "bpms_instance_change":
        handleApprovalEvent(payload)
    case "attendance_check_record":
        handleAttendanceEvent(payload)
    }

    // 钉钉要求返回 success 加密串
    response := encryptResponse("success", os.Getenv("DINGTALK_AES_KEY"))
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{"msg_signature": response})
}

5. 三种场景对比总结

维度群机器人Outgoing 机器人事件订阅
开发门槛⭐ 极低⭐⭐ 低⭐⭐⭐⭐ 高
验签方式无 / 加签HMAC-SHA256AES-CBC + HMAC
消息方向单向:你 → 群双向:用户 ⇋ 你单向:钉钉 → 你
交互复杂度纯推送需解析指令、回复状态变更通知
企业权限不需要不需要需要企业管理员授权
典型场景告警通知智能助手 / 客服机器人审批/考勤同步

6. 常见问题排查

#问题排查修复
1“token is not exist”access_token 错误或机器人被删除重新添加机器人获取新 token
2推送成功但群里看不到机器人被禁言 / 不在群内检查群设置,重新邀请机器人
3加签验证失败时间戳格式错误或密钥不对确认 timestamp 是毫秒级,密钥无空格
4Outgoing 回调收不到回调 URL 无公网访问权限使用 ngrok / Cloudflare Tunnel 暴露
5事件订阅解密失败AES Key 长度不是 43 字符重新生成 aes_key,确保 Base64 正确
6“msg_signature 不匹配”token / aes_key / 时间戳不匹配检查三个参数是否与应用配置一致

7. 下一步


本文全场约 3,200 词,提供 群机器人推送、Outgoing 回调验签、事件订阅解密的完整 Go 代码,以及 3 种场景对比表6 项常见问题排查,可直接用于钉钉集成开发项目。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「saas」更多文章