WEBHOOK
处理 Webhook 请求
在您的服务器上正确接收、验证和处理翠鸟 Webhook POST 请求
处理器设计原则
高质量的 Webhook 处理器应遵循以下原则:
- 快速响应:收到请求后立即返回 200,业务处理逻辑放入异步队列,避免超时
- 幂等设计:同一事件可能因网络问题重复投递,通过事件 ID 去重确保幂等
- 签名验证:处理任何业务逻辑前先验证签名真实性
- 未知事件容忍:对于未订阅或未知的事件类型,静默忽略并返回 200
Java(Spring Boot)示例
JAVA
@RestController
@RequestMapping("/webhooks")
public class CuiniaoWebhookController {
@Autowired
private WebhookVerifier verifier;
@Autowired
private AsyncLeadProcessor leadProcessor;
@PostMapping(value = "/cuiniao", consumes = "application/json")
public ResponseEntity<Void> handle(
HttpServletRequest request,
@RequestBody byte[] rawBody,
@RequestHeader("X-Cuiniao-Signature") String signature,
@RequestHeader("X-Cuiniao-Timestamp") String timestamp
) {
// 1. 签名验证
if (!verifier.verify(new String(rawBody), signature, timestamp)) {
return ResponseEntity.status(400).build();
}
// 2. 解析事件
WebhookEvent event = objectMapper.readValue(rawBody, WebhookEvent.class);
// 3. 幂等检查(通过 Redis / DB 记录已处理事件ID)
if (eventDeduplicator.isProcessed(event.getId())) {
return ResponseEntity.ok().build();
}
// 4. 异步处理,立即返回 200
switch (event.getType()) {
case "lead.created":
leadProcessor.onLeadCreated(event.getData());
break;
case "lead.status_updated":
leadProcessor.onLeadStatusUpdated(event.getData());
break;
default:
// 未知事件类型:忽略
break;
}
eventDeduplicator.markProcessed(event.getId());
return ResponseEntity.ok().build();
}
}
Spring Boot 默认会将请求体解析为对象,这会导致签名验证失败(因为原始字节已被消费)。确保使用
@RequestBody byte[] 接收原始字节,或配置 HttpMessageConverter 缓存请求体。Python(FastAPI)示例
PYTHON
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
from cuiniao.webhook import WebhookVerifier
import json, os
app = FastAPI()
verifier = WebhookVerifier(os.environ["CUINIAO_WEBHOOK_SECRET"])
@app.post("/webhooks/cuiniao")
async def handle_webhook(
request: Request,
background_tasks: BackgroundTasks
):
raw_body = await request.body()
signature = request.headers.get("X-Cuiniao-Signature", "")
timestamp = request.headers.get("X-Cuiniao-Timestamp", "")
# 签名验证
if not verifier.verify(raw_body, signature, timestamp):
raise HTTPException(status_code=400, detail="Invalid signature")
event = json.loads(raw_body)
# 异步处理,立即返回 200
background_tasks.add_task(process_event, event)
return {"received": True}
async def process_event(event: dict):
event_type = event.get("type")
if event_type == "lead.created":
lead = event["data"]
# 处理新线索逻辑...
await notify_sales_team(lead)
elif event_type == "lead.status_updated":
# 同步到 CRM...
pass
Node.js(Express)示例
JAVASCRIPT
const express = require('express');
const { WebhookVerifier } = require('@cuiniao/sdk');
const app = express();
const verifier = new WebhookVerifier(process.env.CUINIAO_WEBHOOK_SECRET);
// 必须使用 express.raw() 保留原始请求体用于签名验证
app.post('/webhooks/cuiniao', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['x-cuiniao-signature'];
const ts = req.headers['x-cuiniao-timestamp'];
if (!verifier.verify(req.body, sig, ts)) {
return res.status(400).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body);
// 立即返回 200,异步处理
res.status(200).json({ received: true });
setImmediate(async () => {
try {
await processEvent(event);
} catch (err) {
console.error('Webhook processing error:', err);
}
});
});
async function processEvent(event) {
switch (event.type) {
case 'lead.created':
await createLeadInCRM(event.data);
await sendSlackNotification(event.data);
break;
case 'resource.published':
await updateContentIndex(event.data);
break;
}
}