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

如何在Azure Functions v4 JavaScript中获取原始Body用于Twitch EventSub的HMAC验证

问题:Azure Functions Node.js v4中无法获取原始请求体验证Twitch EventSub签名

我需要生成匹配Twitch EventSub验证请求的HMAC签名,但始终无法获取到能生成匹配签名的原始请求体。由于Azure Functions Node.js v4移除了request.rawBody,我尝试了request.text()、request.json()后再stringify、request.arrayBuffer()处理流、request.blob(),以及直接将await request.text()加入HMAC的update操作,均未成功。

以下是我使用的代码:

app.http('twitchCommandHandler', {
    methods: ['GET', 'POST'],
    authLevel: 'anonymous',
    handler: async (request, context) => {
        const signature = request.headers.get('Twitch-Eventsub-Message-Signature');
        const timestamp = request.headers.get('Twitch-Eventsub-Message-Timestamp').toLowerCase();
        const messageId = request.headers.get('Twitch-Eventsub-Message-Id').toLowerCase();
        const messageType = request.headers.get('Twitch-Eventsub-Message-Type').toLowerCase();
        const body = await request.text();  

        const message = messageId + timestamp + body;
        const hmac = 'sha256=' + (crypto.createHmac('sha256', process.env.TWITCH_WEBHOOK_SECRET).update(message).digest('hex'));

        const verifyHmac = crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(signature));

        
        if (verifyHmac) {
            return { status: 200, body: body.challenge, headers: { 'Content-Type': 'text/plain' } };
        } else {
            return { status: 401 };
        }
    }
});

我尝试了Azure Functions中所有获取请求Body的方法,包括直接使用request.body,但生成的HMAC均无法与Twitch提供的签名匹配。期望获取用于生成签名的原始Body,以完成签名验证。


解决方案

关键问题分析

  1. Header值被错误修改:对Twitch-Eventsub-Message-Id和Twitch-Eventsub-Message-Timestamp执行toLowerCase()会破坏签名生成的原始参数,Twitch签名计算依赖header的原始大小写值。
  2. 请求体处理方式错误:request.text()会对原始字节流做编码转换(比如自动处理换行符、BOM),导致请求体内容和Twitch发送的原始字节不一致,最终HMAC计算错误。

修正后的代码

const crypto = require('crypto');

app.http('twitchCommandHandler', {
    methods: ['GET', 'POST'],
    authLevel: 'anonymous',
    handler: async (request, context) => {
        const signature = request.headers.get('Twitch-Eventsub-Message-Signature');
        const timestamp = request.headers.get('Twitch-Eventsub-Message-Timestamp');
        const messageId = request.headers.get('Twitch-Eventsub-Message-Id');
        const messageType = request.headers.get('Twitch-Eventsub-Message-Type')?.toLowerCase();

        // 获取原始请求体字节流,避免编码转换
        const bodyBuffer = Buffer.from(await request.arrayBuffer());
        // 仅在需要解析challenge时转成UTF-8字符串
        const bodyStr = bodyBuffer.toString('utf8');

        // 字节级拼接参数:保证和Twitch签名计算的原始内容完全一致
        const messageBuffer = Buffer.concat([
            Buffer.from(messageId, 'utf8'),
            Buffer.from(timestamp, 'utf8'),
            bodyBuffer
        ]);

        // 计算HMAC签名
        const hmac = crypto.createHmac('sha256', process.env.TWITCH_WEBHOOK_SECRET)
            .update(messageBuffer)
            .digest('hex');
        const expectedSignature = `sha256=${hmac}`;

        // 安全比较签名,避免时序攻击
        const isValid = signature && crypto.timingSafeEqual(
            Buffer.from(expectedSignature),
            Buffer.from(signature)
        );

        if (isValid) {
            if (messageType === 'webhook_callback_verification') {
                const bodyObj = JSON.parse(bodyStr);
                return { 
                    status: 200, 
                    body: bodyObj.challenge, 
                    headers: { 'Content-Type': 'text/plain' } 
                };
            }
            return { status: 200 };
        } else {
            return { status: 401 };
        }
    }
});

核心修正点

  • 保留Header原始值:移除对messageId和timestamp的toLowerCase()处理,确保签名计算参数和Twitch一致。
  • 原始字节流获取:用request.arrayBuffer()获取未经过编码转换的请求体字节,直接转为Buffer用于HMAC计算,保证字节级一致性。
  • 字节级拼接:通过Buffer.concat()拼接所有参数的字节,避免字符串拼接可能带来的编码差异。
  • 按需解析请求体:仅在验证回调场景下解析请求体获取challenge,其他场景不修改原始内容。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 03:07:03