Slack 机器人构建器
使用 Bolt 框架构建 Slack 应用,支持 Python、JavaScript 和 Java。 涵盖 Block Kit 丰富 UI、交互组件、斜杠命令、 事件处理、OAuth 安装流程和 Workflow Builder 集成。 专注于生产级 Slack 应用的最佳实践。
模式
Bolt 应用基础模式
Bolt 框架是 Slack 推荐的应用构建方式。 它处理认证、事件路由、请求验证和 HTTP 请求处理,让你专注于应用逻辑。
核心优势:
- 几行代码即可处理事件
- 内置安全检查和载荷验证
- 组织化、一致的模式
- 适用于实验和生产环境
支持语言:Python、JavaScript(Node.js)、Java
使用场景:开始任何新的 Slack 应用、从旧版 Slack API 迁移、构建生产级 Slack 集成
# Python Bolt 应用
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
import os
# 使用环境变量中的令牌初始化
app = App(
token=os.environ["SLACK_BOT_TOKEN"],
signing_secret=os.environ["SLACK_SIGNING_SECRET"]
)
# 处理包含 "hello" 的消息
@app.message("hello")
def handle_hello(message, say):
"""响应包含 'hello' 的消息。"""
user = message["user"]
say(f"Hey there <@{user}>!")
# 处理斜杠命令
@app.command("/ticket")
def handle_ticket_command(ack, body, client):
"""处理 /ticket 斜杠命令。"""
# 立即确认(3 秒内)
ack()
# 打开创建工单的模态框
client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"callback_id": "ticket_modal",
"title": {"type": "plain_text", "text": "Create Ticket"},
"submit": {"type": "plain_text", "text": "Submit"},
"blocks": [
{
"type": "input",
"block_id": "title_block",
"element": {
"type": "plain_text_input",
"action_id": "title_input"
},
"label": {"type": "plain_text", "text": "Title"}
},
{
"type": "input",
"block_id": "desc_block",
"element": {
"type": "plain_text_input",
"multiline": True,
"action_id": "desc_input"
},
"label": {"type": "plain_text", "text": "Description"}
},
{
"type": "input",
"block_id": "priority_block",
"element": {
"type": "static_select",
"action_id": "priority_select",
"options": [
{"text": {"type": "plain_text", "text": "Low"}, "value": "low"},
{"text": {"type": "plain_text", "text": "Medium"}, "value": "medium"},
{"text": {"type": "plain_text", "text": "High"}, "value": "high"}
]
},
"label": {"type": "plain_text", "text": "Priority"}
}
]
}
)
# 处理模态框提交
@app.view("ticket_modal")
def handle_ticket_submission(ack, body, client, view):
"""处理工单模态框提交。"""
ack()
# 从视图中提取值
values = view["state"]["values"]
title = values["title_block"]["title_input"]["value"]
desc = values["desc_block"]["desc_input"]["value"]
priority = values["priority_block"]["priority_select"]["selected_option"]["value"]
user_id = body["user"]["id"]
# 在系统中创建工单
ticket_id = create_ticket(title, desc, priority, user_id)
# 通知用户
client.chat_postMessage(
channel=user_id,
text=f"Ticket #{ticket_id} created: {title}"
)
# 处理按钮点击
@app.action("approve_button")
def handle_approval(ack, body, client):
"""处理审批按钮点击。"""
ack()
# 从操作中获取上下文
user = body["user"]["id"]
action_value = body["actions"][0]["value"]
# 更新消息以移除交互元素
# (最佳实践:防止重复点击)
client.chat_update(
channel=body["channel"]["id"],
ts=body["message"]["ts"],
text=f"Approved by <@{user}>",
blocks=[] # 移除交互块
)
# 监听 app_home_opened 事件
@app.event("app_home_opened")
def update_home_tab(client, event):
"""用户打开主页标签时更新。"""
client.views_publish(
user_id=event["user"],
view={
"type": "home",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Welcome to the Ticket Bot!*"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": "Create Ticket"},
"action_id": "create_ticket_button"
}
]
}
]
}
)
# Socket 模式用于开发(无需公网 URL)
if __name__ == "__main__":
handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
handler.start()
# 生产环境请使用 HTTP 模式配合 Web 服务器
# from flask import Flask, request
# from slack_bolt.adapter.flask import SlackRequestHandler
#
# flask_app = Flask(__name__)
# handler = SlackRequestHandler(app)
#
# @flask_app.route("/slack/events", methods=["POST"])
# def slack_events():
# return handler.handle(request)
反模式
- 未在 3 秒内确认请求
- 在 ack 处理器中执行阻塞操作
- 在源代码中硬编码令牌
- 开发时未使用 Socket 模式
Block Kit UI 模式
Block Kit 是 Slack 的 UI 框架,用于构建丰富的交互式消息。 使用块(sections、actions、inputs)和元素 (按钮、菜单、文本输入)组合消息。
限制:
- 每条消息最多 50 个块
- 模态框/主页标签最多 100 个块
- 块文本限制为 3000 个字符
使用 Block Kit Builder 原型设计:https://app.slack.com/block-kit-builder
使用场景:构建丰富的消息布局、为消息添加交互组件、在模态框中创建表单、构建主页标签体验
from slack_bolt import App
import os
app = App(token=os.environ["SLACK_BOT_TOKEN"])
def build_notification_blocks(incident: dict) -> list:
"""为事件通知构建 Block Kit 块。"""
severity_emoji = {
"critical": ":red_circle:",
"high": ":large_orange_circle:",
"medium": ":large_yellow_circle:",
"low": ":white_circle:"
}
return [
# 头部
{
"type": "header",
"text": {
"type": "plain_text",
"text": f"{severity_emoji.get(incident['severity'], '')} Incident Alert"
}
},
# 详情部分
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": f"*Incident:*\n{incident['title']}"
},
{
"type": "mrkdwn",
"text": f"*Severity:*\n{incident['severity'].upper()}"
},
{
"type": "mrkdwn",
"text": f"*Service:*\n{incident['service']}"
},
{
"type": "mrkdwn",
"text": f"*Reported:*\n<!date^{incident['timestamp']}^{date_short} {time}|{incident['timestamp']}>"
}
]
},
# 描述
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*Description:*\n{incident['description'][:2000]}"
}
},
# 分隔线
{"type": "divider"},
# 操作按钮
{
"type": "actions",
"block_id": f"incident_actions_{incident['id']}",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": "Acknowledge"},
"style": "primary",
"action_id": "acknowledge_incident",
"value": incident['id']
},
{
"type": "button",
"text": {"type": "plain_text", "text": "Resolve"},
"style": "danger",
"action_id": "resolve_incident",
"value": incident['id'],
"confirm": {
"title": {"type": "plain_text", "text": "Resolve Incident?"},
"text": {"type": "mrkdwn", "text": "Are you sure this incident is resolved?"},
"confirm": {"type": "plain_text", "text": "Yes, Resolve"},
"deny": {"type": "plain_text", "text": "Cancel"}
}
},
{
"type": "button",
"text": {"type": "plain_text", "text": "View Details"},
"action_id": "view_incident",
"value": incident['id'],
"url": f"https://incidents.example.com/{incident['id']}"
}
]
},
# 上下文页脚
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": f"Incident ID: {incident['id']} | <https://runbook.example.com/{incident['service']}|View Runbook>"
}
]
}
]
def send_incident_notification(channel: str, incident: dict):
"""使用 Block Kit 发送事件通知。"""
blocks = build_notification_blocks(incident)
app.client.chat_postMessage(
channel=channel,
text=f"Incident: {incident['title']}", # 通知的回退文本
blocks=blocks
)
# 处理按钮操作
@app.action("acknowledge_incident")
def handle_acknowledge(ack, body, client):
"""处理事件确认。"""
ack()
incident_id = body["actions"][0]["value"]
user = body["user"]["id"]
# 更新你的系统
acknowledge_incident(incident_id, user)
# 更新消息显示确认状态
original_blocks = body["message"]["blocks"]
# 在上下文中添加确认信息
original_blocks[-1]["elements"].append({
"type": "mrkdwn",
"text": f":white_check_mark: Acknowledged by <@{user}>"
})
# 移除确认按钮(防止重复点击)
action_block = next(b for b in original_blocks if b.get("block_id", "").startswith("incident_actions"))
action_block["elements"] = [e for e in action_block["elements"] if e["action_id"] != "acknowledge_incident"]
client.chat_update(
channel=body["channel"]["id"],
ts=body["message"]["ts"],
blocks=original_blocks
)
# 交互式选择菜单
def build_user_selector_blocks():
"""构建带用户选择器的块。"""
return [
{
"type": "section",
"text": {"type": "mrkdwn", "text": "Assign this task:"},
"accessory": {
"type": "users_select",
"action_id": "assign_user",
"placeholder": {"type": "plain_text", "text": "Select assignee"}
}
}
]
# 溢出菜单用于更多选项
def build_task_blocks(task: dict):
"""构建带溢出菜单的任务块。"""
return [
{
"type": "section",
"text": {"type": "mrkdwn", "text": f"*{task['title']}*"},
"accessory": {
"type": "overflow",
"action_id": "task_overflow",
"options": [
{
"text": {"type": "plain_text", "text": "Edit"},
"value": f"edit_{task['id']}"
},
{
"text": {"type": "plain_text", "text": "Delete"},
"value": f"delete_{task['id']}"
},
{
"text": {"type": "plain_text", "text": "Share"},
"value": f"share_{task['id']}"
}
]
}
}
]
反模式
- 超过每条消息 50 个块的限制
- 未提供回退文本以支持无障碍
- 硬编码 action_id(需要时使用动态 ID)
- 未对按钮点击进行幂等处理
OAuth 安装模式
让用户通过 OAuth 2.0 在其工作区安装你的应用。 Bolt 处理大部分 OAuth 流程,但你需要配置它 并安全存储令牌。
核心 OAuth 概念:
- Scopes 定义权限(只请求需要的最小权限)
- 令牌是工作区特定的
- 安装数据必须持久化存储
- 用户可以后续添加 scopes(增量式)
70% 的用户在面对过多权限请求时会放弃安装 — 只请求你需要的!
使用场景:将应用分发到多个工作区、构建公开的 Slack 应用、企业级集成
from slack_bolt import App
from slack_bolt.oauth.oauth_settings import OAuthSettings
from slack_sdk.oauth.installation_store import FileInstallationStore
from slack_sdk.oauth.state_store import FileOAuthStateStore
import os
# 生产环境请使用数据库支持的存储
# 例如:PostgreSQL、MongoDB、Redis
class DatabaseInstallationStore:
"""在数据库中存储安装数据。"""
async def save(self, installation):
"""用户完成 OAuth 时保存安装数据。"""
await db.installations.upsert({
"team_id": installation.team_id,
"enterprise_id": installation.enterprise_id,
"bot_token": encrypt(installation.bot_token),
"bot_user_id": installation.bot_user_id,
"bot_scopes": installation.bot_scopes,
"user_id": installation.user_id,
"installed_at": installation.installed_at
})
async def find_installation(self, *, enterprise_id, team_id, user_id=None, is_enterprise_install=False):
"""查找工作区的安装数据。"""
record = await db.installations.find_one({
"team_id": team_id,
"enterprise_id": enterprise_id
})
if record:
return Installation(
bot_token=decrypt(record["bot_token"]),
# ... 其他字段
)
return None
# 初始化支持 OAuth 的应用
app = App(
signing_secret=os.environ["SLACK_SIGNING_SECRET"],
oauth_settings=OAuthSettings(
client_id=os.environ["SLACK_CLIENT_ID"],
client_secret=os.environ["SLACK_CLIENT_SECRET"],
scopes=[
"channels:history",
"channels:read",
"chat:write",
"commands",
"users:read"
],
user_scopes=[], # 如需要则设置用户令牌 scopes
installation_store=DatabaseInstallationStore(),
state_store=FileOAuthStateStore(expiration_seconds=600)
)
)
# OAuth 路由由 Bolt 自动处理
# /slack/install - 发起 OAuth 流程
# /slack/oauth_redirect - 处理回调
# Flask 集成
from flask import Flask, request
from slack_bolt.adapter.flask import SlackRequestHandler
flask_app = Flask(__name__)
handler = SlackRequestHandler(app)
@flask_app.route("/slack/install", methods=["GET"])
def install():
return handler.handle(request)
@flask_app.route("/slack/oauth_redirect", methods=["GET"])
def oauth_redirect():
return handler.handle(request)
@flask_app.route("/slack/events", methods=["POST"])
def slack_events():
return handler.handle(request)
# 处理安装成功/失败
@app.oauth_success
def handle_oauth_success(args):
"""OAuth 成功完成时调用。"""
installation = args["installation"]
# 发送欢迎消息
app.client.chat_postMessage(
token=installation.bot_token,
channel=installation.user_id,
text="Thanks for installing! Type /help to get started."
)
return "Installation successful! You can close this window."
@app.oauth_failure
def handle_oauth_failure(args):
"""OAuth 失败时调用。"""
error = args.get("error", "Unknown error")
return f"Installation failed: {error}"
# Scope 管理 - 需要时请求额外的 scopes
def request_additional_scopes(team_id: str, new_scopes: list):
"""
生成 URL 让用户添加 scopes。
注意:现有令牌保留旧的 scopes。
用户必须重新授权以获取新的 scopes。
"""
base_url = "https://slack.com/oauth/v2/authorize"
params = {
"client_id": os.environ["SLACK_CLIENT_ID"],
"scope": ",".join(new_scopes),
"team": team_id
}
return f"{base_url}?{urlencode(params)}"
反模式
- 提前请求不必要的 scopes
- 以明文存储令牌
- 未验证 OAuth state 参数(CSRF 风险)
- 假设配置更改后令牌自动获得新的 scopes
Socket 模式
Socket 模式允许你的应用通过 WebSocket 而非 公网 HTTP 端点接收事件。非常适合开发和 位于防火墙后的应用。
优势:
- 无需公网 URL
- 可在企业防火墙后工作
- 更简单的本地开发
- 实时双向通信
限制:不推荐用于高流量生产应用。
使用场景:本地开发、企业防火墙后的应用、有安全约束的内部工具、原型设计和测试
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
import os
# Socket 模式需要应用级令牌(xapp-...)
# 在 App Settings > Basic Information > App-Level Tokens 中创建
# 需要 'connections:write' scope
app = App(token=os.environ["SLACK_BOT_TOKEN"])
@app.message("hello")
def handle_hello(message, say):
say(f"Hey <@{message['user']}>!")
@app.command("/status")
def handle_status(ack, say):
ack()
say("All systems operational!")
@app.event("app_mention")
def handle_mention(event, say):
say(f"You mentioned me, <@{event['user']}>!")
if __name__ == "__main__":
# SocketModeHandler 管理 WebSocket 连接
handler = SocketModeHandler(
app,
os.environ["SLACK_APP_TOKEN"] # xapp-... 令牌
)
print("Starting Socket Mode...")
handler.start()
# 异步应用
from slack_bolt.async_app import AsyncApp
from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler
import asyncio
async_app = AsyncApp(token=os.environ["SLACK_BOT_TOKEN"])
@async_app.message("hello")
async def handle_hello_async(message, say):
await say(f"Hey <@{message['user']}>!")
async def main():
handler = AsyncSocketModeHandler(async_app, os.environ["SLACK_APP_TOKEN"])
await handler.start_async()
if __name__ == "__main__":
asyncio.run(main())
反模式
- 在高流量生产应用中使用 Socket 模式
- 未处理 WebSocket 断开连接
- 忘记创建应用级令牌
- 使用机器人令牌而非应用令牌
Workflow Builder 步骤模式
使用你的应用驱动的自定义步骤扩展 Slack 的 Workflow Builder。 用户可以在其无代码工作流中包含你的自定义步骤。
工作流步骤可以:
- 从用户收集输入
- 执行自定义逻辑
- 为后续步骤输出数据
使用场景:与 Workflow Builder 集成、让非技术用户使用你的功能、构建可复用的自动化组件
from slack_bolt import App
from slack_bolt.workflows.step import WorkflowStep
import os
app = App(
token=os.environ["SLACK_BOT_TOKEN"],
signing_secret=os.environ["SLACK_SIGNING_SECRET"]
)
# 定义工作流步骤
def edit(ack, step, configure):
"""用户在 Workflow Builder 中添加/编辑步骤时调用。"""
ack()
# 显示配置模态框
blocks = [
{
"type": "input",
"block_id": "ticket_type",
"element": {
"type": "static_select",
"action_id": "type_select",
"options": [
{"text": {"type": "plain_text", "text": "Bug"}, "value": "bug"},
{"text": {"type": "plain_text", "text": "Feature"}, "value": "feature"},
{"text": {"type": "plain_text", "text": "Task"}, "value": "task"}
]
},
"label": {"type": "plain_text", "text": "Ticket Type"}
},
{
"type": "input",
"block_id": "title_input",
"element": {
"type": "plain_text_input",
"action_id": "title"
},
"label": {"type": "plain_text", "text": "Title"}
},
{
"type": "input",
"block_id": "assignee_input",
"element": {
"type": "users_select",
"action_id": "assignee"
},
"label": {"type": "plain_text", "text": "Assignee"}
}
]
configure(blocks=blocks)
def save(ack, view, update):
"""用户保存步骤配置时调用。"""
ack()
values = view["state"]["values"]
# 定义输入(来自用户的配置)
inputs = {
"ticket_type": {
"value": values["ticket_type"]["type_select"]["selected_option"]["value"]
},
"title": {
"value": values["title_input"]["title"]["value"]
},
"assignee": {
"value": values["assignee_input"]["assignee"]["selected_user"]
}
}
# 定义输出(供后续步骤使用)
outputs = [
{
"name": "ticket_id",
"type": "text",
"label": "Created Ticket ID"
},
{
"name": "ticket_url",
"type": "text",
"label": "Ticket URL"
}
]
update(inputs=inputs, outputs=outputs)
def execute(step, complete, fail):
"""步骤在工作流中运行时调用。"""
inputs = step["inputs"]
try:
# 获取输入值
ticket_type = inputs["ticket_type"]["value"]
title = inputs["title"]["value"]
assignee = inputs["assignee"]["value"]
# 在系统中创建工单
ticket = create_ticket(
type=ticket_type,
title=title,
assignee=assignee
)
# 完成并输出结果
complete(outputs={
"ticket_id": ticket["id"],
"ticket_url": ticket["url"]
})
except Exception as e:
fail(error={"message": str(e)})
# 注册工作流步骤
create_ticket_step = WorkflowStep(
callback_id="create_ticket_step",
edit=edit,
save=save,
execute=execute
)
app.step(create_ticket_step)
反模式
- 在 execute 中未调用 complete() 或 fail()
- 长时间运行的操作没有进度更新
- 在 execute 中未验证输入
- 在输出中暴露敏感数据
关键陷阱
缺少 3 秒确认(超时)
严重程度:CRITICAL
场景:处理斜杠命令、快捷方式或交互组件
症状: 用户看到"This command timed out"或"Something went wrong。" 即使你的代码运行了,操作也永远不会完成。 开发环境正常但生产环境失败。
原因: Slack 要求所有交互请求在 3 秒内确认:
- 斜杠命令
- 按钮/选择菜单点击
- 模态框提交
- 快捷方式
如果你在响应之前做了任何慢操作(数据库、API 调用、LLM), 你会错过窗口期。即使你的机器人最终正确处理了请求,Slack 也会显示错误。
推荐修复:
## 立即确认,稍后处理
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
import threading
app = App(token=os.environ["SLACK_BOT_TOKEN"])
@app.command("/slow-task")
def handle_slow_task(ack, command, client, respond):
# 立即确认 - 在任何处理之前
ack("Processing your request...")
# 在后台执行慢操作
def do_work():
result = call_slow_api(command["text"]) # 耗时 10 秒
respond(f"Done! Result: {result}")
threading.Thread(target=do_work).start()
@app.view("modal_submission")
def handle_modal(ack, body, client, view):
# 模态框使用 response_action 确认
ack(response_action="clear") # 或使用 "update" 更新视图
# 在后台处理
user_id = body["user"]["id"]
values = view["state"]["values"]
# ... 慢处理
## 对于 Bolt 框架 - 使用懒监听器
# Bolt 通过懒监听器自动处理 ack()
@app.command("/slow-task")
def handle_slow_task(ack, command, respond):
ack() # 仍然先调用 ack()!
@handle_slow_task.lazy
def process_slow_task(command, respond):
# 在 ack 之后运行,可以任意长时间
result = slow_operation(command["text"])
respond(result)
未验证 OAuth State 参数(CSRF)
严重程度:CRITICAL
场景:实现 OAuth 安装流程
症状: 机器人似乎正常工作,但你容易受到 CSRF 攻击。 攻击者可以诱骗用户安装恶意配置。
原因: OAuth state 参数防止 CSRF 攻击。流程:
- 你生成随机 state,存储它,发送给 Slack
- 用户在 Slack 中授权
- Slack 使用 code + state 重定向回来
- 你必须验证 state 与你存储的匹配
没有这个,攻击者可以制作恶意 OAuth URL 并诱骗 管理员使用攻击者的授权码完成流程。
推荐修复:
## 正确的 state 验证
import secrets
from flask import Flask, request, session, redirect
from slack_sdk.oauth import AuthorizeUrlGenerator
from slack_sdk.oauth.state_store import FileOAuthStateStore
app = Flask(__name__)
app.secret_key = os.environ["SESSION_SECRET"]
# 使用 Slack SDK 的 state 存储(生产环境推荐 Redis)
state_store = FileOAuthStateStore(
expiration_seconds=300, # 5 分钟
base_dir="./oauth_states"
)
@app.route("/slack/install")
def install():
# 生成加密安全的 state
state = state_store.issue()
# 存储在会话中用于验证
session["oauth_state"] = state
authorize_url = AuthorizeUrlGenerator(
client_id=os.environ["SLACK_CLIENT_ID"],
scopes=["channels:history", "chat:write"],
user_scopes=[]
).generate(state)
return redirect(authorize_url)
@app.route("/slack/oauth/callback")
def oauth_callback():
# 关键:验证 state
received_state = request.args.get("state")
stored_state = session.get("oauth_state")
if not received_state or received_state != stored_state:
return "Invalid state parameter - possible CSRF attack", 403
# 同时使用 state_store.consume() 确保一次性使用
if not state_store.consume(received_state):
return "State already used or expired", 403
# 现在可以安全地用 code 换取令牌
code = request.args.get("code")
# ... 完成 OAuth 流程
暴露机器人/用户令牌
严重程度:CRITICAL
场景:存储或记录 Slack 令牌
症状: 你的机器人发送了未授权的消息。攻击者读取私有 频道。令牌在日志、git 历史或客户端代码中被发现。
原因: Slack 令牌提供其 scopes 范围内的完全访问权限:
- 机器人令牌(xoxb-*):访问已安装的工作区
- 用户令牌(xoxp-*):以该特定用户身份访问
- 应用级令牌(xapp-*):Socket 模式连接
常见暴露点:
- 硬编码在源代码中
- 在错误消息中记录
- 发送到前端/客户端
- 在数据库中未加密存储
推荐修复:
## 永远不要硬编码或记录令牌
# 错误 - 永远不要这样做
client = WebClient(token="xoxb-12345-...")
# 正确 - 使用环境变量
client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
# 错误 - 记录令牌
logger.error(f"API call failed with token {token}")
# 正确 - 永远不要记录令牌
logger.error(f"API call failed for team {team_id}")
# 错误 - 将令牌发送到前端
return {"token": bot_token}
# 正确 - 只发送前端需要的内容
return {"channels": channel_list}
## 在数据库中加密令牌
from cryptography.fernet import Fernet
class TokenStore:
def __init__(self, encryption_key: str):
self.cipher = Fernet(encryption_key)
def save_token(self, team_id: str, token: str):
encrypted = self.cipher.encrypt(token.encode())
db.execute(
"INSERT INTO installations (team_id, encrypted_token) VALUES (?, ?)",
(team_id, encrypted)
)
def get_token(self, team_id: str) -> str:
row = db.execute(
"SELECT encrypted_token FROM installations WHERE team_id = ?",
(team_id,)
).fetchone()
return self.cipher.decrypt(row[0]).decode()
## 如果令牌泄露则轮换
1. Slack API > Your App > OAuth & Permissions
2. 点击泄露令牌的 "Rotate"
3. 立即更新所有部署
4. 检查 Slack 审计日志是否有未授权访问
请求不必要的 OAuth Scopes
严重程度:HIGH
场景:为你的应用配置 OAuth scopes
症状: 用户因可怕的权限警告而犹豫安装。 安装率降低。安全部门阻止部署。 应用被 Slack App Directory 拒绝。
原因: 每个 OAuth scope 授予特定权限。请求超过 你需要的:
- 使安装同意屏幕看起来很可怕
- 如果令牌泄露会增加攻击面
- 可能违反企业安全策略
- 可能导致你的应用被 App Directory 拒绝
常见的过度请求:
- 只需要
chat:write却请求了admin - 只向一个频道发消息却请求了
channels:read - 不需要邮箱却请求了
users:read.email
推荐修复:
## 请求最小必需的 scopes
# 对于简单的通知机器人
MINIMAL_SCOPES = [
"chat:write", # 发布消息
"channels:join", # 加入公开频道(如需要)
]
# 基本通知不需要:
# - channels:read(除非你列出频道)
# - users:read(除非你查找用户)
# - channels:history(除非你读取消息)
# 对于斜杠命令机器人
SLASH_COMMAND_SCOPES = [
"commands", # 注册斜杠命令
"chat:write", # 响应命令
]
# 对于响应 @提及的机器人
MENTION_BOT_SCOPES = [
"app_mentions:read", # 接收 @提及
"chat:write", # 回复提及
]
## 按用例分类的 scope 参考
# | 用例 | 必需的 Scopes |
# |----------|-----------------|
# | 发布消息 | `chat:write` |
# | 斜杠命令 | `commands` |
# | 响应 @提及 | `app_mentions:read`, `chat:write` |
# | 读取频道消息 | `channels:history`(公开), `groups:history`(私有) |
# | 读取用户信息 | `users:read` |
# | 打开模态框 | `commands` 或从事件触发 |
# | 添加表情回应 | `reactions:write` |
# | 上传文件 | `files:write` |
## 渐进式 scope 请求
# 从最小 scopes 开始
INITIAL_SCOPES = ["chat:write", "commands"]
# 仅在需要时请求额外的 scopes
@app.command("/enable-reactions")
def enable_reactions(ack, client, command):
ack()
# 检查是否已有该 scope
auth_result = client.auth_test()
# 如果缺少 reactions:write,提示重新授权
if needs_additional_scope:
# 发送用户去重新授权以获取额外 scope
pass
超出 Block Kit 限制
严重程度:MEDIUM
场景:使用 Block Kit 构建复杂的消息 UI
症状: 消息发送失败并显示"invalid_blocks"错误。 模态框无法打开。消息被意外截断。
原因: Block Kit 有严格的限制,但并不总是显而易见:
- 每条消息/模态框 50 个块
- 每个文本块 3000 个字符
- 每个 actions 块 10 个元素
- 每个选择菜单 100 个选项
- 模态框:50 个块,总计 24KB
- 主页标签:100 个块
超出这些限制会导致静默失败或晦涩的错误。
推荐修复:
## 了解并遵守限制
# Block Kit 限制常量
BLOCK_KIT_LIMITS = {
"blocks_per_message": 50,
"blocks_per_modal": 50,
"blocks_per_home": 100,
"text_block_chars": 3000,
"elements_per_actions": 10,
"options_per_select": 100,
"modal_total_bytes": 24 * 1024, # 24KB
}
def validate_blocks(blocks: list) -> tuple[bool, str]:
"""发送前验证块。"""
if len(blocks) > BLOCK_KIT_LIMITS["blocks_per_message"]:
return False, f"Too many blocks: {len(blocks)} > 50"
for block in blocks:
if block.get("type") == "section":
text = block.get("text", {}).get("text", "")
if len(text) > BLOCK_KIT_LIMITS["text_block_chars"]:
return False, f"Text too long: {len(text)} > 3000"
if block.get("type") == "actions":
elements = block.get("elements", [])
if len(elements) > BLOCK_KIT_LIMITS["elements_per_actions"]:
return False, f"Too many actions: {len(elements)} > 10"
return True, "OK"
# 对长内容分页
def paginate_blocks(blocks: list, page: int = 0, per_page: int = 45):
"""对块进行分页并添加导航。"""
start = page * per_page
end = start + per_page
page_blocks = blocks[start:end]
# 添加分页控件
if len(blocks) > per_page:
page_blocks.append({
"type": "actions",
"elements": [
{"type": "button", "text": {"type": "plain_text", "text": "Previous"},
"action_id": f"page_{page-1}", "disabled": page == 0},
{"type": "button", "text": {"type": "plain_text", "text": "Next"},
"action_id": f"page_{page+1}",
"disabled": end >= len(blocks)}
]
})
return page_blocks
在生产环境使用 Socket 模式
严重程度:HIGH
场景:将 Slack 机器人部署到生产环境
症状: 机器人在开发环境正常但生产环境不可靠。 事件丢失。连接断开。无法水平扩展。
原因: Socket 模式是为开发设计的:
- 每个应用单个 WebSocket 连接
- 无法扩展到多个实例
- 连接可能断开(需要重连逻辑)
- 没有内置负载均衡
对于多实例或高流量的生产环境, HTTP webhook 更可靠。
推荐修复:
## Socket 模式:仅用于开发
if os.environ.get("ENVIRONMENT") == "development":
from slack_bolt.adapter.socket_mode import SocketModeHandler
handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
handler.start()
## 生产环境:使用 HTTP 端点
# 生产环境使用 HTTP(Flask 示例)
from slack_bolt.adapter.flask import SlackRequestHandler
from flask import Flask, request
flask_app = Flask(__name__)
handler = SlackRequestHandler(app)
@flask_app.route("/slack/events", methods=["POST"])
def slack_events():
return handler.handle(request)
@flask_app.route("/slack/commands", methods=["POST"])
def slack_commands():
return handler.handle(request)
@flask_app.route("/slack/interactions", methods=["POST"])
def slack_interactions():
return handler.handle(request)
## 如果必须在生产环境使用 Socket 模式
from slack_bolt.adapter.socket_mode import SocketModeHandler
import time
class RobustSocketHandler:
def __init__(self, app, app_token):
self.app = app
self.app_token = app_token
self.handler = None
def start(self):
while True:
try:
self.handler = SocketModeHandler(self.app, self.app_token)
self.handler.start()
except Exception as e:
logger.error(f"Socket Mode disconnected: {e}")
time.sleep(5) # 重连前退避
未验证请求签名
严重程度:CRITICAL
场景:从 Slack 接收 webhook
症状: 攻击者可以向你的 webhook 端点发送伪造请求。 伪造的斜杠命令。处理了伪造的事件通知。
原因: Slack 使用你的签名密钥通过 X-Slack-Signature 头对所有请求签名。没有验证,任何知道你 webhook URL 的人都可以发送伪造请求。
这与 OAuth 令牌不同 - 签名验证的是请求 来自 Slack,而不是你有权调用 Slack。
推荐修复:
## Bolt 自动处理
from slack_bolt import App
# 提供 signing_secret 时 Bolt 自动验证签名
app = App(
token=os.environ["SLACK_BOT_TOKEN"],
signing_secret=os.environ["SLACK_SIGNING_SECRET"]
)
# 你处理器中的所有请求都已验证
## 手动验证(如果不使用 Bolt)
import hmac
import hashlib
import time
from flask import Flask, request, abort
SIGNING_SECRET = os.environ["SLACK_SIGNING_SECRET"]
def verify_slack_signature(request):
timestamp = request.headers.get("X-Slack-Request-Timestamp", "")
signature = request.headers.get("X-Slack-Signature", "")
# 拒绝旧时间戳(防止重放攻击)
if abs(time.time() - int(timestamp)) > 60 * 5:
return False
# 计算预期签名
sig_basestring = f"v0:{timestamp}:{request.get_data(as_text=True)}"
expected_sig = "v0=" + hmac.new(
SIGNING_SECRET.encode(),
sig_basestring.encode(),
hashlib.sha256
).hexdigest()
# 恒定时间比较
return hmac.compare_digest(expected_sig, signature)
@app.route("/slack/events", methods=["POST"])
def slack_events():
if not verify_slack_signature(request):
abort(403)
# 安全处理
验证检查
硬编码 Slack 令牌
严重程度:ERROR
Slack 令牌绝不应硬编码
消息:检测到硬编码的 Slack 令牌。请使用环境变量。
源代码中的签名密钥
严重程度:ERROR
签名密钥应在环境变量中
消息:硬编码的签名密钥。请使用 os.environ['SLACK_SIGNING_SECRET']。
未验证签名的 Webhook
严重程度:ERROR
Slack webhook 必须验证 X-Slack-Signature
消息:Webhook 未验证签名。请使用 Bolt 或手动验证。
客户端代码中的 Slack 令牌
严重程度:ERROR
永远不要向浏览器暴露 Slack 令牌
消息:Slack 凭证在客户端暴露。请仅在服务端使用。
确认前的慢操作
严重程度:WARNING
ack() 必须在慢操作之前调用
消息:ack() 之前有慢操作。请先调用 ack(),然后处理。
缺少确认调用
严重程度:WARNING
交互处理器必须调用 ack()
消息:处理器缺少 ack() 调用。必须在 3 秒内确认。
未验证 State 的 OAuth
严重程度:ERROR
OAuth 回调必须验证 state 参数
消息:OAuth 未验证 state。容易受到 CSRF 攻击。
未加密的令牌存储
严重程度:WARNING
令牌应加密存储
消息:令牌未加密存储。请加密静态令牌。
请求 Admin Scopes
严重程度:WARNING
除非绝对必要,否则避免使用 admin scopes
消息:请求了 admin scope。请使用最小必需的 scopes。
可能未使用的 Scope
严重程度:INFO
检查所有请求的 scopes 是否都被使用
消息:请求了 users:read.email 但可能未使用邮箱。请验证必要性。
协作
委派触发器
- 用户需要 AI 驱动的 Slack 机器人 -> llm-architect(集成 LLM 构建对话式 Slack 机器人)
- 用户需要语音通知 -> twilio-communications(将 Slack 告警升级为短信或语音通话)
- 用户需要工作流自动化 -> workflow-automation(Slack 作为 n8n/Temporal 工作流的触发器/动作)
- 用户需要 Discord 机器人 -> discord-bot-architect(跨平台机器人架构)
- 用户需要完整认证系统 -> auth-specialist(OAuth、工作区管理、企业 SSO)
- 用户需要机器人数据的数据库 -> postgres-wizard(存储安装数据、用户偏好、消息历史)
- 用户需要高可用性 -> devops(扩展 webhook、监控、告警)
使用场景
- 用户提到或暗示:slack bot
- 用户提到或暗示:slack app
- 用户提到或暗示:bolt framework
- 用户提到或暗示:block kit
- 用户提到或暗示:slash command
- 用户提到或暗示:slack webhook
- 用户提到或暗示:slack workflow
- 用户提到或暗示:slack interactive
- 用户提到或暗示:slack oauth
局限性
- 仅当任务明确匹配上述范围时才使用此技能。
- 不要将输出视为环境特定验证、测试或专家审查的替代品。
- 如果缺少所需输入、权限、安全边界或成功标准,请停下来要求澄清。