如何验证SendGrid入站解析Webhook发送至服务器的POST请求?
验证SendGrid Inbound Parse Webhook请求的最佳方案
SendGrid官方提供了一套基于HMAC-SHA256的签名验证机制,这是验证请求合法性最可靠的方式,能有效防止伪造请求和重放攻击。下面是具体的实现步骤和注意事项:
一、先在SendGrid控制台开启签名验证
首先登录你的SendGrid账号,进入 Settings > Mail Settings > Inbound Parse,找到你配置的对应whatever@mydomain.com的解析规则,确保"Enable Signatures"选项处于开启状态。这一步是让SendGrid在发送POST请求时自动带上签名相关的HTTP头。
二、服务器端验证逻辑实现
SendGrid会在每个请求里带上两个关键HTTP头:
X-Sendgrid-Timestamp:请求发送的Unix时间戳(秒级)X-Sendgrid-Signature:用你的API密钥生成的HMAC签名
你需要在服务器端完成以下三步验证:
1. 验证时间戳,防止重放攻击
攻击者可能会重复发送旧的合法请求,所以先检查时间戳和当前服务器时间的差值,一般建议限制在5分钟(300秒)以内,超出范围直接拒绝请求。
2. 生成待签名的字符串
把时间戳和请求的原始请求体拼接成 {timestamp}.{raw_request_body} 的格式——这里一定要用原始的请求体,不能是解析后的JSON对象,因为格式变化(比如空格、换行符)会导致签名不匹配。
3. 计算签名并对比
用你的SendGrid API密钥作为密钥,HMAC-SHA256算法对拼接后的字符串生成哈希值,再和请求头里的X-Sendgrid-Signature对比,一致则说明请求合法。
三、代码示例(常用语言)
Node.js/Express 示例
注意要先配置中间件获取原始请求体:
const express = require('express'); const crypto = require('crypto'); const app = express(); // 配置中间件,保留原始请求体 app.use(express.raw({ type: 'application/json' })); const SENDGRID_API_KEY = process.env.SENDGRID_API_KEY; // 用环境变量存储密钥,不要硬编码 function validateSendgridRequest(req) { const timestamp = req.headers['x-sendgrid-timestamp']; const signature = req.headers['x-sendgrid-signature']; if (!timestamp || !signature) return false; // 验证时间戳 const now = Math.floor(Date.now() / 1000); if (Math.abs(now - parseInt(timestamp)) > 300) return false; // 生成待签名内容 const signedPayload = `${timestamp}.${req.body.toString()}`; // 计算签名 const computedSignature = crypto .createHmac('sha256', SENDGRID_API_KEY) .update(signedPayload) .digest('hex'); // 安全对比签名(避免时序攻击) return crypto.timingSafeEqual(Buffer.from(computedSignature), Buffer.from(signature)); } // 处理Inbound Parse请求的路由 app.post('/your-webhook-endpoint', (req, res) => { if (!validateSendgridRequest(req)) { return res.status(403).send('Invalid request'); } // 验证通过,处理邮件数据 const emailData = JSON.parse(req.body.toString()); // ... 导入到主应用的逻辑 res.status(200).send('OK'); }); app.listen(3000);
Python/Django 示例
import hmac import hashlib import time import json import os from django.http import HttpResponseForbidden, HttpResponse from django.views.decorators.csrf import csrf_exempt SENDGRID_API_KEY = os.environ.get('SENDGRID_API_KEY') def validate_sendgrid_request(request): timestamp = request.headers.get('X-Sendgrid-Timestamp') signature = request.headers.get('X-Sendgrid-Signature') if not timestamp or not signature: return False # 验证时间戳 now = int(time.time()) if abs(now - int(timestamp)) > 300: return False # 获取原始请求体 raw_body = request.body.decode('utf-8') signed_payload = f"{timestamp}.{raw_body}".encode('utf-8') # 计算签名并对比(用compare_digest避免时序攻击) computed_signature = hmac.new( SENDGRID_API_KEY.encode('utf-8'), signed_payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(computed_signature, signature) @csrf_exempt def inbound_parse_webhook(request): if request.method != 'POST': return HttpResponse(status=405) if not validate_sendgrid_request(request): return HttpResponseForbidden('Invalid request') # 处理邮件数据 email_data = json.loads(request.body) # ... 导入到主应用的逻辑 return HttpResponse('OK')
四、关键注意事项
- API密钥安全:绝对不要把API密钥硬编码在代码里,用环境变量、密钥管理服务存储,避免泄露。
- 原始请求体:很多Web框架会自动解析请求体,一定要配置中间件保留原始内容,否则签名会不匹配。
- 时序攻击防护:用语言内置的安全对比方法(比如Node.js的
timingSafeEqual、Python的hmac.compare_digest),不要直接用==对比字符串,防止攻击者通过时序差异破解签名。
内容的提问来源于stack exchange,提问作者kdeez
相关产品推荐
相关产品推荐

