Slack Webhook 集成实战:Incoming Webhook、Events API 与 Block Kit(2025 完整指南)

Slack Webhook 集成完整指南:Incoming Webhook 推送、Events API 签名验证(Signing Secret)、Slash Commands、Block Kit 消息格式。含 Go/Node.js 完整代码。

TL;DR:Slack 的 Webhook 生态最成熟——Incoming Webhook(推送到频道)、Events API(接收 Slack 事件)、Slash Commands(斜杠命令)。核心签名方式使用 HMAC-SHA256 + Signing Secret,时间戳防重放机制与 Stripe 类似但细节不同。


1. Slack Webhook 类型速览

类型方向触发场景认证方式适用
Incoming Webhook你 → Slack服务端推送消息Token(URL 本身含 token)通知类到频道
Events APISlack → 你消息/反应/频道变动Signing Secret + timestamp交互式机器人
Slash Commands用户 → 你用户输入 /deploySigning Secret + token快捷指令
Interactive Components用户 → 你点击按钮/菜单Signing Secret卡片交互

2. Incoming Webhook(推送到频道)

2.1 配置步骤

  1. 打开 Slack APICreate New AppFrom scratch
  2. 进入 Incoming Webhooks → 激活 → Add New Webhook to Workspace
  3. 选择目标频道 → 授权
  4. 获得 Webhook URL
https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX

2.2 Go 推送代码

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

type SlackWebhook struct {
    URL string
}

type SlackMessage struct {
    Text        string       `json:"text,omitempty"`
    Blocks      []SlackBlock `json:"blocks,omitempty"`
    Attachments []Attachment `json:"attachments,omitempty"`
}

type SlackBlock struct {
    Type string      `json:"type"`
    Text *BlockText  `json:"text,omitempty"`
}

type BlockText struct {
    Type string `json:"type"` // plain_text / mrkdwn
    Text string `json:"text"`
}

func (s *SlackWebhook) SendText(text string) error {
    msg := SlackMessage{Text: text}
    body, _ := json.Marshal(msg)
    resp, err := http.Post(s.URL, "application/json", bytes.NewReader(body))
    if err != nil {
        return err
    }
    defer resp.Body.Close()

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

// 使用示例
func main() {
    slack := &SlackWebhook{
        URL: "https://hooks.slack.com/services/T00000000/B00000000/XXXX",
    }
    slack.SendText("🚀 部署通知:服务 v1.2.3 已发布到生产环境")
}

2.3 Block Kit 消息( richer 格式)

func (s *SlackWebhook) SendBlockMessage(title, content string) error {
    msg := SlackMessage{
        Blocks: []SlackBlock{
            {
                Type: "header",
                Text: &BlockText{Type: "plain_text", Text: title},
            },
            {
                Type: "section",
                Text: &BlockText{Type: "mrkdwn", Text: content},
            },
            {
                Type: "divider",
            },
            {
                Type: "section",
                Text: &BlockText{
                    Type: "mrkdwn",
                    Text: "*时间*: 2025-01-15 10:30:00\n*发布人*: deploy-bot",
                },
            },
        },
    }

    body, _ := json.Marshal(msg)
    resp, _ := http.Post(s.URL, "application/json", bytes.NewReader(body))
    resp.Body.Close()
    return nil
}

Block Kit 消息效果:

┌────────────────────────────────────┐
│ 🚀 部署通知                         │
├────────────────────────────────────┤
│ 服务 v1.2.3 已发布到生产环境        │
├────────────────────────────────────┤
│ 时间: 2025-01-15 10:30:00          │
│ 发布人: deploy-bot                  │
└────────────────────────────────────┘

3. Events API(接收 Slack 事件)

3.1 配置步骤

  1. Slack App → Event Subscriptions → 启用
  2. 配置 Request URLhttps://yourapp.com/webhooks/slack
  3. 订阅事件:
    • app_mention — 用户 @Bot 时触发
    • message.channels — 频道消息
    • reaction_added — 表情反应
  4. OAuth & Permissions 中添加 chat:write scope

3.2 签名验证(Signing Secret)

Slack 的签名是拼接字符串后做 HMAC-SHA256

签名输入: "v0:" + timestamp + ":" + raw_body
签名: hmac_sha256(SigningSecret, base_string)
Header: X-Slack-Signature: v0=<hex_signature>
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "io"
    "net/http"
    "strconv"
    "time"
)

func verifySlackSignature(r *http.Request, signingSecret string) error {
    // 读取原始 body
    body, err := io.ReadAll(r.Body)
    if err != nil {
        return err
    }
    // 必须恢复 body 供后续读取
    r.Body = io.NopCloser(bytes.NewReader(body))

    // 获取签名和时间戳
    signature := r.Header.Get("X-Slack-Signature")
    timestamp := r.Header.Get("X-Slack-Request-Timestamp")

    if signature == "" || timestamp == "" {
        return fmt.Errorf("missing signature headers")
    }

    // 防重放:时间戳必须在 5 分钟内
    ts, err := strconv.ParseInt(timestamp, 10, 64)
    if err != nil {
        return fmt.Errorf("invalid timestamp")
    }
    if time.Since(time.Unix(ts, 0)) > 5*time.Minute {
        return fmt.Errorf("request too old")
    }

    // 计算签名
    baseString := fmt.Sprintf("v0:%s:%s", timestamp, string(body))
    mac := hmac.New(sha256.New, []byte(signingSecret))
    mac.Write([]byte(baseString))
    expectedSignature := "v0=" + hex.EncodeToString(mac.Sum(nil))

    if !hmac.Equal([]byte(signature), []byte(expectedSignature)) {
        return fmt.Errorf("signature mismatch")
    }

    return nil
}

3.3 Go Handler

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

    // ① 签名验证
    if err := verifySlackSignature(r, os.Getenv("SLACK_SIGNING_SECRET")); err != nil {
        http.Error(w, err.Error(), http.StatusUnauthorized)
        return
    }

    var event struct {
        Token     string `json:"token"`
        Challenge string `json:"challenge"` // URL 验证
        Type      string `json:"type"`      // url_verification / event_callback
        Event     struct {
            Type    string `json:"type"`
            User    string `json:"user"`
            Text    string `json:"text"`
            Channel string `json:"channel"`
            Ts      string `json:"ts"`
        } `json:"event"`
    }
    json.Unmarshal(body, &event)

    // ② URL 验证(首次配置时用)
    if event.Type == "url_verification" {
        w.Header().Set("Content-Type", "text/plain")
        w.Write([]byte(event.Challenge))
        return
    }

    // ③ 处理事件
    if event.Type == "event_callback" {
        switch event.Event.Type {
        case "app_mention":
            handleAppMention(event.Event.Channel, event.Event.Text, event.Event.User)
        case "message":
            // 忽略 Bot 自己的消息防止循环
            if event.Event.User != "" {
                handleMessage(event.Event.Channel, event.Event.Text)
            }
        }
    }

    w.WriteHeader(http.StatusOK)
}

func handleAppMention(channel, text, userID string) {
    reply := fmt.Sprintf("<@%s> 收到你的消息:%s", userID, text)
    sendSlackMessage(channel, reply)
}

func sendSlackMessage(channel, text string) {
    token := os.Getenv("SLACK_BOT_TOKEN")
    payload := map[string]interface{}{
        "channel": channel,
        "text":    text,
    }

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

    http.DefaultClient.Do(req)
}

4. Slash Commands(斜杠命令)

4.1 配置

  1. Slack App → Slash CommandsCreate New Command
  2. 配置:
    • Command: /deploy
    • Request URL: https://yourapp.com/webhooks/slack/commands
    • Short Description: “Deploy service to production”

4.2 Slash Command Handler

func handleSlackCommand(w http.ResponseWriter, r *http.Request) {
    if err := verifySlackSignature(r, os.Getenv("SLACK_SIGNING_SECRET")); err != nil {
        http.Error(w, err.Error(), http.StatusUnauthorized)
        return
    }

    r.ParseForm()
    command := r.FormValue("command")   // /deploy
    text := r.FormValue("text")          // "production v1.2.3"
    userID := r.FormValue("user_id")
    channelID := r.FormValue("channel_id")
    responseURL := r.FormValue("response_url")

    switch command {
    case "/deploy":
        go processDeployment(text, responseURL, channelID)
        // 立即返回,异步处理
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{
            "text": fmt.Sprintf("🚀 正在部署 %s,请稍候...", text),
        })

    case "/status":
        status := getSystemStatus()
        json.NewEncoder(w).Encode(map[string]string{
            "text": status,
        })
    }
}

func processDeployment(target, responseURL, channel string) {
    // 异步执行部署
    time.Sleep(10 * time.Second) // 模拟部署

    // 通过 response_url 发送结果(有效期 30 分钟)
    result := map[string]string{
        "text": fmt.Sprintf("✅ %s 部署完成!", target),
    }
    body, _ := json.Marshal(result)
    http.Post(responseURL, "application/json", bytes.NewReader(body))
}

4.3 Block Kit 交互按钮

func sendInteractiveMessage(channel string) {
    payload := map[string]interface{}{
        "channel": channel,
        "text":    "请选择操作:",
        "blocks": []map[string]interface{}{
            {
                "type": "section",
                "text": map[string]string{
                    "type": "mrkdwn",
                    "text": "⚠️ 确认要回滚到 v1.2.2 吗?",
                },
            },
            {
                "type": "actions",
                "elements": []map[string]interface{}{
                    {
                        "type": "button",
                        "text": map[string]string{
                            "type": "plain_text",
                            "text": "确认回滚",
                        },
                        "style": "danger",
                        "value": "rollback_v1.2.2",
                        "action_id": "confirm_rollback",
                    },
                    {
                        "type": "button",
                        "text": map[string]string{
                            "type": "plain_text",
                            "text": "取消",
                        },
                        "value": "cancel",
                        "action_id": "cancel_action",
                    },
                },
            },
        },
    }

    body, _ := json.Marshal(payload)
    req, _ := http.NewRequest("POST", "https://slack.com/api/chat.postMessage", bytes.NewReader(body))
    req.Header.Set("Authorization", "Bearer "+os.Getenv("SLACK_BOT_TOKEN"))
    req.Header.Set("Content-Type", "application/json")
    http.DefaultClient.Do(req)
}

5. Slack vs 钉钉/飞书对比

维度Slack钉钉飞书
签名格式v0:timestamp:body HMAC-SHA256timestamp\nsecret HMAC-SHA256timestamp\nsecret HMAC-SHA256
时间戳单位毫秒
防重放窗口5 分钟1 小时1 小时
消息格式Block Kit(最丰富)Markdown + 有限卡片Interactive Card(较丰富)
推送方式Webhook URLWebhook URL + TokenWebhook URL + Sign
OAuthOAuth 2.0(最标准)企业内部应用 OAuthOAuth 2.0
生态成熟度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

6. 常见问题排查

#问题排查修复
1“invalid_auth”Bot Token 过期或 scope 不足检查 OAuth & Permissions 中的 scope
2收到事件但回复不被展示Bot 不在频道中在频道中 @Bot 或手动邀请
3Events API URL 验证失败未正确返回 challenge返回 Content-Type: text/plain + Challenge 值
4签名验证失败时间戳格式不匹配或 body 被 middleware 解析先读原始 body 再验签
5Slash Command 超时处理时间 > 3s立即返回 ack,通过 response_url 异步回复
6Bot 消息循环触发Events API 收到自己的消息过滤 event.user 为 Bot ID 的消息

7. 下一步


本文全场约 3,500 词,提供 Incoming Webhook 推送、Events API 签名验证、Slash Commands、Block Kit 交互的完整 Go 代码,以及 Slack vs 钉钉/飞书对比表,可直接用于 Slack 集成开发项目。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「saas」更多文章