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

Python实现Zoom Webhook URL验证时返回未授权请求的问题排查

问题

搭建Zoom Webhook URL验证端点时,始终返回错误:Unauthorized request to Zoom Webhook sample.,以下是使用FastAPI编写的Python代码:

@router.post('/webhook')
async def webhook(request: Request):
    headers = dict(request.headers)
    body = await request.json()
    
    print(headers)
    print(body)
    # construct the message string
    message = f"v0:{headers['x-zm-request-timestamp']}:{body}"
    # hash the message string with your Webhook Secret Token and prepend the version semantic
    hash_for_verify = hmac.new(ZOOM_SECRET_TOKEN.encode(), message.encode(), hashlib.sha256).hexdigest()
    signature = f"v0={hash_for_verify}"

    # validating the request came from Zoom
    if headers['x-zm-signature'] == signature:

        # Zoom validating you control the webhook endpoint
        if body['event'] == 'endpoint.url_validation':
            hash_for_validate = hmac.new(ZOOM_SECRET_TOKEN.encode(), body['payload']['plainToken'].encode(), hashlib.sha256).hexdigest()

            response = {
                'message': {
                    'plainToken': body['payload']['plainToken'],
                    'encryptedToken': hash_for_validate
                },
                'status': 200
            }

            print(response['message'])
            return response['message']
        else:
            response = {'message': 'Authorized request to Zoom Webhook sample.', 'status': 200}

            print(response['message'])
            return response

            # business logic here, example make API request to Zoom or 3rd party

    else:
        response = {'message': 'Unauthorized request to Zoom Webhook sample.', 'status': 401}

        print(response['message'])
        return response

尝试修改哈希编码后问题依旧,请求排查未授权错误原因。

排查思路与修复方案

出现Unauthorized错误的核心原因是签名验证不通过,以下是具体问题点和修复方法:

1. 错误使用解析后的JSON对象构造签名字符串(致命问题)

代码中直接将解析后的Python字典body拼接到签名字符串中,这会导致字符串格式与Zoom发送的原始请求体完全不一致——Python字典的字符串表示用单引号,而原始JSON用双引号,再加上缩进、空格等差异,最终生成的哈希值必然和Zoom的签名不匹配。

修复方法:获取原始请求体字节数据,用其字符串形式构造签名:

# 替换原有的body = await request.json()
raw_body = await request.body()
body = json.loads(raw_body)  # 仍需解析JSON处理业务逻辑,但签名用原始字节

# 构造签名字符串时使用原始body的UTF-8编码字符串
message = f"v0:{timestamp}:{raw_body.decode('utf-8')}"

2. HTTP Header大小写兼容问题

HTTP协议中Header名称不区分大小写,但部分Web框架(如FastAPI)会将Header转换为小写形式,直接通过headers['x-zm-request-timestamp']或headers['x-zm-signature']取值可能抛出KeyError,导致签名逻辑中断。

修复方法:用get方法兼容不同大小写的Header:

timestamp = request.headers.get('x-zm-request-timestamp') or request.headers.get('X-Zm-Request-Timestamp')
zoom_signature = request.headers.get('x-zm-signature') or request.headers.get('X-Zm-Signature')

if not timestamp or not zoom_signature:
    return {'message': 'Missing required headers', 'status': 400}

3. 时间戳过期校验(可选但建议)

Zoom的签名机制要求请求时间戳与当前时间偏差不能超过5分钟(防止重放攻击),若服务器时间与Zoom服务器时间偏差过大,也会导致验证失败。

修复方法:添加时间戳校验逻辑:

import time

current_timestamp = int(time.time())
if abs(current_timestamp - int(timestamp)) > 300:  # 5分钟=300秒
    return {'message': 'Timestamp expired', 'status': 401}

完整修复后的代码示例

import hmac
import hashlib
import json
import time
from fastapi import Request, APIRouter

router = APIRouter()
ZOOM_SECRET_TOKEN = "你的Webhook密钥"

@router.post('/webhook')
async def webhook(request: Request):
    # 获取关键Header,兼容大小写
    timestamp = request.headers.get('x-zm-request-timestamp') or request.headers.get('X-Zm-Request-Timestamp')
    zoom_signature = request.headers.get('x-zm-signature') or request.headers.get('X-Zm-Signature')
    
    if not timestamp or not zoom_signature:
        response = {'message': 'Missing required headers', 'status': 400}
        print(response['message'])
        return response
    
    # 校验时间戳是否过期
    current_timestamp = int(time.time())
    if abs(current_timestamp - int(timestamp)) > 300:
        response = {'message': 'Timestamp expired', 'status': 401}
        print(response['message'])
        return response
    
    # 获取原始请求体,用于签名验证
    raw_body = await request.body()
    body = json.loads(raw_body)
    
    print(dict(request.headers))
    print(body)
    
    # 构造签名字符串
    message = f"v0:{timestamp}:{raw_body.decode('utf-8')}"
    hash_for_verify = hmac.new(ZOOM_SECRET_TOKEN.encode(), message.encode(), hashlib.sha256).hexdigest()
    signature = f"v0={hash_for_verify}"

    # 验证签名
    if zoom_signature == signature:
        if body['event'] == 'endpoint.url_validation':
            hash_for_validate = hmac.new(ZOOM_SECRET_TOKEN.encode(), body['payload']['plainToken'].encode(), hashlib.sha256).hexdigest()
            response_msg = {
                'plainToken': body['payload']['plainToken'],
                'encryptedToken': hash_for_validate
            }
            print(response_msg)
            return response_msg
        else:
            response = {'message': 'Authorized request to Zoom Webhook sample.', 'status': 200}
            print(response['message'])
            return response
    else:
        response = {'message': 'Unauthorized request to Zoom Webhook sample.', 'status': 401}
        print(response['message'])
        return response

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 08:39:59