如何在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,以完成签名验证。
解决方案
关键问题分析
- Header值被错误修改:对
Twitch-Eventsub-Message-Id和Twitch-Eventsub-Message-Timestamp执行toLowerCase()会破坏签名生成的原始参数,Twitch签名计算依赖header的原始大小写值。 - 请求体处理方式错误:
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
相关产品推荐
相关产品推荐

