本文档总结使用 Python 开发飞书机器人事件回调服务器的完整流程和注意事项。

一、配置流程

1.1 飞书后台配置

  1. 登录 飞书开发者后台
  2. 创建或选择企业自建应用
  3. 进入 事件与回调 页面

1.2 加密策略配置

事件与回调 -> 加密策略 页签:

参数说明是否必须
Encrypt Key用于加密/解密事件数据,提高传输安全性可选(建议配置)
Verification Token应用验证标识,用于验证事件来源可选(建议配置)

注意

  • Verification Token 是系统自动生成的,无需手动设置
  • Encrypt Key 需要手动设置,建议使用 16 位以上的随机字符串
  • 如果配置了 Encrypt Key,所有推送的事件都会被加密

1.3 事件订阅配置

  1. 进入 事件配置 页签
  2. 点击 订阅方式 旁边的编辑图标
  3. 选择 将事件发送至开发者服务器
  4. 填写请求地址(必须是公网可访问的 URL)
  5. 点击 保存

保存后,飞书会向配置的 URL 发送验证请求。

1.4 添加事件

  1. 已订阅的回调 区域,点击 添加事件
  2. 搜索并添加需要订阅的事件,如:
    • im.message.receive_v1 - 接收消息 v2.0
    • bot.add - 机器人加入群组
    • bot.remove - 机器人离开群组

二、服务器开发

2.1 核心流程

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  飞书服务器   │ ──► │  你的服务器   │ ──► │  业务逻辑    │
└─────────────┘     └─────────────┘     └─────────────┘
       │                   │
       │  1. URL 验证       │
       │  2. 事件推送       │
       │  (加密/明文)       │
       ▼                   ▼

2.2 URL 验证处理

飞书保存回调地址时,会发送验证请求:

请求格式(加密):

{
  "encrypt": "Base64 编码的加密数据"
}

解密后格式:

{
  "challenge": "848d55f5-922e-4a29-a41c-36d4c9ab618b",
  "token": "XlVzQQd7peXa0b2P21kDee4rCAcy1m3D",
  "type": "url_verification"
}

响应要求:

  • 必须在 1 秒内 返回
  • 响应格式:{"challenge": "原样返回挑战码"}
  • HTTP 状态码:200

2.3 事件处理

事件推送格式(解密后):

{
  "challenge": "",
  "token": "XlVzQQd7peXa0b2P21kDee4rCAcy1m3D",
  "type": "event_callback",
  "event": {
    "type": "im.message.receive_v1",
    "message": {...},
    "sender": {...}
  },
  "header": {
    "event_id": "...",
    "event_type": "...",
    "create_time": "..."
  }
}

三、加密解密实现

3.1 加密原理

飞书使用 AES-256-CBC 加密算法:

  1. 使用 SHA256 对 Encrypt Key 进行哈希得到密钥 key(32 字节)
  2. 生成 16 字节的随机 IV
  3. 使用 keyIV 对事件内容加密
  4. IV 和加密数据拼接后 Base64 编码

3.2 Python 解密代码

import base64
import hashlib
import json
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

def decrypt_message(encrypt_key: str, encrypted_data: str) -> dict:
    """解密飞书加密消息"""
    # Base64 解码
    encrypted_bytes = base64.b64decode(encrypted_data)
    
    # IV 是前 16 字节
    iv = encrypted_bytes[:16]
    ciphertext = encrypted_bytes[16:]
    
    # 密钥是 Encrypt Key 的 SHA256 哈希(32 字节)
    key = hashlib.sha256(encrypt_key.encode("utf-8")).digest()
    
    # AES-256-CBC 解密
    cipher = AES.new(key, AES.MODE_CBC, iv)
    decrypted_padded = cipher.decrypt(ciphertext)
    
    # 去除 PKCS7 填充
    padding_len = decrypted_padded[-1]
    decrypted = decrypted_padded[:-padding_len]
    
    return json.loads(decrypted.decode("utf-8"))

3.3 依赖安装

pip install pycryptodome

四、签名验证(可选)

如果需要进一步验证请求来源,可以进行签名校验:

4.1 签名生成规则

飞书使用以下公式生成签名:

signature = SHA256(timestamp + nonce + encryptKey + body)

4.2 请求头参数

参数说明
X-Lark-Request-Timestamp请求时间戳
X-Lark-Request-Nonce随机 nonce
X-Lark-Signature签名值

4.3 验证代码

import hashlib
import hmac
import time

def validate_signature(timestamp: str, nonce: str, body: str, 
                       header_signature: str, encrypt_key: str) -> bool:
    # 时间戳验证(容错 60 秒)
    if abs(int(time.time()) - int(timestamp)) > 60:
        return False
    
    # 计算签名
    sign_string = f"{timestamp}{nonce}{encrypt_key}{body}"
    signature = hashlib.sha256(sign_string.encode("utf-8")).hexdigest()
    
    # 比较签名(常量时间比较)
    return hmac.compare_digest(signature.lower(), header_signature.lower())

五、注意事项

5.1 配置相关

问题解决方案
Challenge code 没有返回确保 1 秒内返回 challenge,检查解密逻辑
解密失败检查 Encrypt Key 是否正确,使用 AES-256-CBC
签名验证失败检查密钥、时间戳、请求体是否被修改

5.2 安全相关

  1. 防重放攻击:缓存并检查 nonce,拒绝重复请求
  2. 时间戳验证:拒绝超过 60 秒的请求
  3. 签名校验:验证请求来源合法性
  4. HTTPS:生产环境建议使用 HTTPS

5.3 性能相关

  1. 响应时间:URL 验证必须在 1 秒内响应
  2. 异步处理:事件处理建议异步执行,避免阻塞
  3. 幂等性:事件可能重复推送,业务逻辑需支持幂等

5.4 调试技巧

  1. 打印原始请求:将飞书的 POST 请求完整打印出来
  2. 本地测试:使用 ngrok 等工具将本地服务暴露到公网
  3. 日志记录:记录解密前后的数据,便于排查问题

六、完整示例

6.1 项目结构

/mnt/d/SourceCodes/Python/
├── main.py              # 主服务器代码
├── config.example.py    # 配置示例
└── FeiShu-WebHook.md    # 本文档

6.2 启动服务器

# 安装依赖
pip install pycryptodome

# 编辑配置(修改 CONFIG 中的 encrypt_key 和 verification_token)
vim main.py

# 启动服务器
python main.py

6.3 测试验证

  1. 确保服务器在公网可访问
  2. 在飞书后台填写回调 URL
  3. 点击 保存,查看验证结果
  4. 验证通过后,添加需要订阅的事件

七、完整示例代码

7.1 自动回复示例

handle_message_event 中添加自动回复逻辑:

def handle_message_event(event: dict) -> dict:
    """处理消息事件"""
    message = event.get("message", {})
    sender = event.get("sender", {})
    
    # 解析消息内容
    content = message.get("content", {})
    if isinstance(content, str):
        content = json.loads(content)
    
    # 获取聊天 ID 和用户信息
    chat_id = message.get("chat_id", "")
    user_text = content.get("text", "")
    open_id = sender.get("sender_id", {}).get("open_id", "")
    
    logger.info(f"收到来自 {open_id} 的消息:{user_text}")
    
    # 自动回复
    if user_text:
        reply_message(chat_id, f"收到你的消息:{user_text}")
    
    return {"status": "success"}

7.2 智能回复示例(集成 AI)

import requests

def call_ai_api(user_text: str) -> str:
    """调用 AI 接口生成回复"""
    # 示例:调用某个 AI API
    url = "https://api.example.com/chat"
    response = requests.post(url, json={"message": user_text})
    result = response.json()
    return result.get("reply", "抱歉,我暂时无法回答")

def handle_message_event(event: dict) -> dict:
    """处理消息事件 - 智能回复版"""
    message = event.get("message", {})
    content = message.get("content", {})
    
    if isinstance(content, str):
        content = json.loads(content)
    
    chat_id = message.get("chat_id", "")
    user_text = content.get("text", "")
    
    # 调用 AI 接口生成回复
    ai_reply = call_ai_api(user_text)
    
    # 发送回复
    reply_message(chat_id, ai_reply)
    
    return {"status": "success"}

八、常见问题

Q1: 验证时返回 401 错误

原因:Encrypt Key 或 Verification Token 配置不匹配

解决:检查飞书后台和代码中的配置是否一致

Q2: 解密后是乱码

原因:密钥派生方式错误

解决:使用完整的 SHA256 哈希(32 字节)作为 AES-256 密钥

Q3: 事件重复推送

原因:网络问题或响应超时导致飞书重试

解决:业务逻辑实现幂等性,使用 event_id 去重

Q4: 如何回复消息

回复消息需要调用飞书的发送消息 API,并且 receive_id_type 参数必须放在 URL 查询参数中:

import requests

# 1. 获取 app_access_token
def get_app_access_token(app_id: str, app_secret: str) -> str:
    url = "https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal"
    data = {"app_id": app_id, "app_secret": app_secret}
    response = requests.post(url, json=data)
    result = response.json()
    return result.get("app_access_token", "")

# 2. 发送消息(注意 receive_id_type 在 URL 中)
def reply_message(chat_id: str, content: str, token: str):
    # receive_id_type 必须放在 URL 查询参数中!
    url = "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id"
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }
    data = {
        "receive_id": chat_id,  # p2p 聊天传 chat_id,群聊也传 chat_id
        "msg_type": "text",
        "content": json.dumps({"text": content})
    }
    response = requests.post(url, headers=headers, json=data)
    return response.json()

# 使用示例
token = get_app_access_token("your_app_id", "your_app_secret")
reply_message("oc_xxx", "收到你的消息", token)

关键点:

  • receive_id_type 必须放在 URL 查询参数中,不是请求体
  • p2p 聊天和群聊都使用 chat_id(以 oc_ 开头)
  • 需要配置应用的 app_idapp_secret

九、参考文档