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 API | Slack → 你 | 消息/反应/频道变动 | Signing Secret + timestamp | 交互式机器人 |
| Slash Commands | 用户 → 你 | 用户输入 /deploy | Signing Secret + token | 快捷指令 |
| Interactive Components | 用户 → 你 | 点击按钮/菜单 | Signing Secret | 卡片交互 |
2. Incoming Webhook(推送到频道)
2.1 配置步骤
- 打开 Slack API → Create New App → From scratch
- 进入 Incoming Webhooks → 激活 → Add New Webhook to Workspace
- 选择目标频道 → 授权
- 获得 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 配置步骤
- Slack App → Event Subscriptions → 启用
- 配置 Request URL:
https://yourapp.com/webhooks/slack - 订阅事件:
app_mention— 用户 @Bot 时触发message.channels— 频道消息reaction_added— 表情反应
- 在 OAuth & Permissions 中添加
chat:writescope
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 配置
- Slack App → Slash Commands → Create New Command
- 配置:
- Command:
/deploy - Request URL:
https://yourapp.com/webhooks/slack/commands - Short Description: “Deploy service to production”
- Command:
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-SHA256 | timestamp\nsecret HMAC-SHA256 | timestamp\nsecret HMAC-SHA256 |
| 时间戳单位 | 秒 | 毫秒 | 秒 |
| 防重放窗口 | 5 分钟 | 1 小时 | 1 小时 |
| 消息格式 | Block Kit(最丰富) | Markdown + 有限卡片 | Interactive Card(较丰富) |
| 推送方式 | Webhook URL | Webhook URL + Token | Webhook URL + Sign |
| OAuth | OAuth 2.0(最标准) | 企业内部应用 OAuth | OAuth 2.0 |
| 生态成熟度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
6. 常见问题排查
| # | 问题 | 排查 | 修复 |
|---|---|---|---|
| 1 | “invalid_auth” | Bot Token 过期或 scope 不足 | 检查 OAuth & Permissions 中的 scope |
| 2 | 收到事件但回复不被展示 | Bot 不在频道中 | 在频道中 @Bot 或手动邀请 |
| 3 | Events API URL 验证失败 | 未正确返回 challenge | 返回 Content-Type: text/plain + Challenge 值 |
| 4 | 签名验证失败 | 时间戳格式不匹配或 body 被 middleware 解析 | 先读原始 body 再验签 |
| 5 | Slash Command 超时 | 处理时间 > 3s | 立即返回 ack,通过 response_url 异步回复 |
| 6 | Bot 消息循环触发 | Events API 收到自己的消息 | 过滤 event.user 为 Bot ID 的消息 |
7. 下一步
- 📖 钉钉 Webhook 集成实战 → — HMAC 加签、群机器人、事件订阅
- 📖 飞书 Webhook 集成实战 → — Encrypt Key 解密、消息卡片
- 📖 Webhook Gateway 设计 → — 统一接入 Slack/钉钉/飞书多平台
- 📖 Webhook 监控告警体系 → — Prometheus + Grafana 监控多平台投递
本文全场约 3,500 词,提供 Incoming Webhook 推送、Events API 签名验证、Slash Commands、Block Kit 交互的完整 Go 代码,以及 Slack vs 钉钉/飞书对比表,可直接用于 Slack 集成开发项目。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。
「saas」更多文章
短链接对 SEO 的影响与优化最佳实践
深度解析短链接对 SEO 的影响,覆盖 HTTP 重定向状态码对 PageRank 的传递差异、品牌短链与公共短链的 SEO 对比、Google 索引机制与实战优化建议,帮助 SEO 从业者和营销人员正确使用短链接。
UTM 参数 + 短链接:追踪每一条营销链路
本文系统讲解 UTM 参数的定义、5 个核心字段详解、命名规范,以及 UTM 与短链接结合的最佳实践。涵盖主流 UTM builder 工具对比、数据分析方法、常见错误规避和高级玩法,帮你建立一套完整的营销追踪工作流。
私域流量运营中的短链接策略:从引流到转化
深度解析短链接在微信、抖音、小红书等私域运营场景中的实战策略,涵盖渠道追踪、裂变增长、防封域名、活码技术、转化漏斗优化等核心方法论,帮助 SaaS 企业和品牌商家从引流到转化构建完整的私域增长闭环。