You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

FastAPI+Zoom Webhook本地正常Docker部署签名验证失败求助

问题:Docker运行FastAPI+Zoom Webhook时签名验证失败

搭建了FastAPI应用接收Zoom Webhook的会议/录制事件,本地通过ngrok转发运行正常,但构建为Docker镜像运行后出现signature rejected(签名验证失败)问题。已确认环境变量一致、依赖版本通过Poetry保持同步、容器时区与主机相同,问题仍未解决。

相关代码与配置

签名验证函数

def signature_verified(self, headers: dict, body: bytes):
    # 从请求头和体中提取必要组件
    timestamp = headers['x-zm-request-timestamp']
    if not timestamp:
        return False  # 缺少时间戳,无法验证
    
    decoded_body = body.decode('utf-8')
    message = f"v0:{timestamp}:{decoded_body}"
    
    print("Constructed Message:", message)

    # 获取Zoom Webhook密钥
    secret_token = os.getenv('ZOOM_WEBHOOK_SECRET_TOKEN')
    if not secret_token:
        print("Secret token is missing.")
        return False
    
    # 创建HMAC SHA-256哈希
    hash_for_verify = hmac.new(secret_token.encode(), message.encode(), hashlib.sha256).hexdigest()
    generated_signature = f"v0={hash_for_verify}"

    print("Computed HMAC:", hash_for_verify)
    human_readable = datetime.fromtimestamp(int(timestamp))
    print("Timestamp from Zoom:", headers['x-zm-request-timestamp'])
    print("Human-readable timestamp:", human_readable)
    print("Generated Signature:", generated_signature)
    print("Expected Signature:", headers.get('x-zm-signature'))

    return headers.get('x-zm-signature') == generated_signature

Webhook路由代码

@app.post("/webhook")
async def webhook(request: Request):
    client_host = request.client.host
    print(f"Request from IP: {client_host}")
    
    body = await request.body()
    headers = request.headers
    try:
        if zoom_client.signature_verified(headers, body):
            event_data = await request.json()

            if event_data["event"] == "endpoint.url_validation":
                logging.info("WEBHOOK: URL validation request received")
                return zoom_client.url_verification(event_data)

            elif event_data["event"] == "session.ended":
                topic = event_data["payload"]["object"]["topic"]
                id = event_data["payload"]["object"]["id"]
                logging.info(f"WEBHOOK: Session ended: {topic}({id}))")
                return {"message": "Received session.ended event."}
            else:
                logging.error("WEBHOOK: Unauthorized request to Webhook: Signature verified rejected")
                return {"message": "Unauthorized request to Zoom Webhook"}, 401

    except Exception as e:
        logging.error(f"WEBHOOK: Error processing webhook: {e}")
        return {"message": "Error processing Zoom Webhook"}, 500

pyproject.toml

[tool.poetry]
name = "zoomerizer"
version = "0.1.0"
description = "An app that summarizes the recordings of zoom meetings. It communicates with Zoom API."
authors = ["<email>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.109.2"
redis = "^5.0.1"
types-redis = "^4.6.0.20240218"
uvicorn = "^0.27.1"
python-dotenv = "^1.0.1"
requests = "^2.31.0"
logging = "^0.4.9.6"
coloredlogs = "^15.0.1"
verboselogs = "^1.7"
SQLAlchemy = "^2.0.27"
langchain = "^0.1.8"
openai = "^1.12.0"
langchain-openai = "^0.0.8"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

Dockerfile

# For more information, please refer to https://aka.ms/vscode-docker-python
FROM python:3-slim

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

ENV POETRY_VERSION=1.8.2
RUN pip install "poetry==$POETRY_VERSION"

COPY pyproject.toml poetry.lock* ./

RUN poetry config virtualenvs.create false \
    && poetry install --no-dev --no-interaction --no-ansi

EXPOSE 8000

COPY . /app

RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser:appuser /app
USER appuser

CMD ["uvicorn", "zoomerizer.main:app", "--host", "0.0.0.0", "--port", "8000"]

运行命令与错误日志

运行命令:

docker run -e TZ=Europe/Berlin -p 8000:8000 --env-file .env zoomerizer

错误日志:

INFO:     192.168.65.1:64127 - "POST /webhook HTTP/1.1" 200 OK
2024-05-30 14:38:40,320 - ERROR - WEBHOOK: Unauthorized request to Webhook: Signature verified rejected

排查与解决方案

1. 确认请求体的一致性

Zoom签名基于原始请求体字节流计算,本地与Docker环境中请求体可能存在格式化差异(如换行符、空格、JSON缩进)。

  • 打印本地和Docker环境下的body原始字节内容(转换为十六进制或直接输出),对比是否完全一致。
  • 确保在验证签名前,未对请求体进行任何修改(如二次解析、编码转换)。

2. 验证环境变量加载正确性

即使使用--env-file,Docker仍可能存在环境变量加载不全的情况:

  • 在签名验证函数中添加print(f"Secret token length: {len(secret_token)}"),对比本地与Docker中密钥长度是否一致(避免打印完整密钥)。
  • 直接在容器内执行echo $ZOOM_WEBHOOK_SECRET_TOKEN,确认变量值正确。

3. 统一Python版本

当前Docker镜像使用python:3-slim,默认版本可能低于pyproject.toml要求的3.11,导致字符串编码或HMAC计算差异:

  • 修改Dockerfile基础镜像为python:3.11-slim,确保Python版本与本地一致。

4. 兼容请求头大小写差异

部分服务器环境会将HTTP头转为小写,导致获取不到正确的Zoom头信息:

  • 修改签名验证函数中的头获取逻辑,兼容大小写:
    timestamp = headers.get('x-zm-request-timestamp') or headers.get('X-Zm-Request-Timestamp')
    expected_signature = headers.get('x-zm-signature') or headers.get('X-Zm-Signature')
    

5. 同步容器系统时间

Zoom签名验证要求时间戳在当前时间的5分钟范围内,容器时间偏差过大也会导致验证失败:

  • 在容器内执行date命令,对比主机时间是否一致。
  • 可在Dockerfile中添加时间同步配置,或使用带NTP同步的基础镜像。

内容的提问来源于stack exchange,提问作者SarahData

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 19:32:03