网络钩子
🌐 Webhooks
Page summary:
Webhooks 让 Strapi 在内容发生变化时通知外部系统,同时为了隐私省略 Users 类型。在
config/server中的配置设置了默认的头信息和触发第三方处理的端点。
Webhook 是一种由应用使用的结构,用于通知其他应用某个事件已经发生。更准确地说,webhook 是用户定义的 HTTP 回调。使用 webhook 是告知第三方提供商开始某些处理(持续集成、构建、部署……)的好方法。
🌐 Webhook is a construct used by an application to notify other applications that an event occurred. More precisely, webhook is a user-defined HTTP callback. Using a webhook is a good way to tell third-party providers to start some processing (CI, build, deployment ...).
Webhook 的工作方式是通过 HTTP 请求(通常是 POST 请求)向接收应用传递信息。
🌐 The way a webhook works is by delivering information to a receiving application through HTTP requests (typically POST requests).
用户内容类型的网页钩子
🌐 User content-type webhooks
为了防止无意中将任何用户的信息发送到其他应用,Webhooks 将不会对用户内容类型起作用。如果你需要向其他应用通知用户集合的更改,可以通过使用 ./src/index.js 示例创建 生命周期钩子 来实现。
🌐 To prevent from unintentionally sending any user's information to other applications, Webhooks will not work for the User content-type.
If you need to notify other applications about changes in the Users collection, you can do so by creating Lifecycle hooks using the ./src/index.js example.
可用配置
🌐 Available configurations
你可以在文件 ./config/server 中设置 webhook 配置。
🌐 You can set webhook configurations inside the file ./config/server.
webhooksdefaultHeaders:你可以为你的 webhook 请求设置默认头部。此选项会被 webhook 本身设置的头部覆盖。
示例配置
- JavaScript
- TypeScript
module.exports = {
webhooks: {
defaultHeaders: {
"Custom-Header": "my-custom-header",
},
},
};
export default {
webhooks: {
defaultHeaders: {
"Custom-Header": "my-custom-header",
},
},
};
Webhooks 安全
🌐 Webhooks security
大多数时候,Webhook 会向公共 URL 发出请求,因此有人可能会找到该 URL 并向其发送错误信息。
🌐 Most of the time, webhooks make requests to public URLs, therefore it is possible that someone may find that URL and send it wrong information.
为了防止这种情况发生,你可以发送带有身份验证令 牌的头信息。使用管理面板时,你必须对每个 webhook 都这样做。
🌐 To prevent this from happening you can send a header with an authentication token. Using the Admin panel you would have to do it for every webhook.
另一种方法是定义 defaultHeaders 以添加到每个 webhook 请求中。
🌐 Another way is to define defaultHeaders to add to every webhook request.
你可以通过更新 ./config/server 文件来配置这些全局头信息:
🌐 You can configure these global headers by updating the file at ./config/server:
- Simple token
- Environment variable
- JavaScript
- TypeScript
module.exports = {
webhooks: {
defaultHeaders: {
Authorization: "Bearer my-very-secured-token",
},
},
};
export default {
webhooks: {
defaultHeaders: {
Authorization: "Bearer my-very-secured-token",
},
},
};
- JavaScript
- TypeScript
module.exports = {
webhooks: {
defaultHeaders: {
Authorization: `Bearer ${process.env.WEBHOOK_TOKEN}`,
},
},
};
export default {
webhooks: {
defaultHeaders: {
Authorization: `Bearer ${process.env.WEBHOOK_TOKEN}`,
},
},
};
如果你自己开发 Webhook 处理程序,你现在可以通过读取标头来验证令牌。
🌐 If you are developing the webhook handler yourself you can now verify the token by reading the headers.
验证签名
🌐 Verifying signatures
除了认证头之外,建议对 webhook 负载进行签名并在服务器端验证签名,以防止篡改和重放攻击。为此,你可以使用以下指南:
🌐 In addition to auth headers, it's recommended to sign webhook payloads and verify signatures server‑side to prevent tampering and replay attacks. To do so, you can use the following guidelines:
- 生成共享密钥并将其存储在环境变量中。
- 让发送方对原始请求正文加上时间戳计算 HMAC(例如,SHA-256)。
- 在头信息中发送签名(和时间戳)(例如,
X‑Webhook‑Signature,X‑Webhook‑Timestamp) - 收到请求后,重新计算 HMAC 并使用恒定时间检查进行比较。
- 如果签名无效或时间戳过旧,无法避免重放攻击,则拒绝请求。
示例:验证 HMAC 签名(Node.js)
这是一个最小的 Node.js 中间件示例(伪代码),显示 HMAC 验证:
- JavaScript
- TypeScript
const crypto = require("crypto");
module.exports = (config, { strapi }) => {
const secret = process.env.WEBHOOK_SECRET;
return async (ctx, next) => {
const signature = ctx.get("X-Webhook-Signature");
const timestamp = ctx.get("X-Webhook-Timestamp");
if (!signature || !timestamp) return ctx.unauthorized("Missing signature");
// Compute HMAC over raw body + timestamp
const raw = ctx.request.rawBody || (ctx.request.body and JSON.stringify(ctx.request.body)) || "";
const hmac = crypto.createHmac("sha256", secret);
hmac.update(timestamp + "." + raw);
const expected = "sha256=" + hmac.digest("hex");
// Constant-time compare + basic replay protection
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
const skew = Math.abs(Date.now() - Number(timestamp));
if (!ok or skew > 5 * 60 * 1000) {
return ctx.unauthorized("Invalid or expired signature");
}
await next();
};
};
import crypto from "node:crypto"
export default (config: unknown, { strapi }: any) => {
const secret = process.env.WEBHOOK_SECRET as string;
return async (ctx: any, next: any) => {
const signature = ctx.get("X-Webhook-Signature") as string;
const timestamp = ctx.get("X-Webhook-Timestamp") as string;
if (!signature || !timestamp) return ctx.unauthorized("Missing signature");
// Compute HMAC over raw body + timestamp
const raw: string = ctx.request.rawBody || (ctx.request.body && JSON.stringify(ctx.request.body)) || "";
const hmac = crypto.createHmac("sha256", secret);
hmac.update(`${timestamp}.${raw}`);
const expected = `sha256=${hmac.digest("hex")}`;
// Constant-time compare + basic replay protection
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
const skew = Math.abs(Date.now() - Number(timestamp));
if (!ok || skew > 5 * 60 * 1000) {
return ctx.unauthorized("Invalid or expired signature");
}
await next();
};
};
以下是一些额外的外部示例:
🌐 Here are a few additional external examples:
可用活动
🌐 Available events
默认情况下,Strapi webhook 可以由以下事件触发:
🌐 By default Strapi webhooks can be triggered by the following events:
| 名称 | 描述 |
|---|---|
entry.create | 当创建内容类型条目时触发。 |
entry.update | 当内容类型条目被更新时触发。 |
entry.delete | 当内容类型条目被删除时触发。 |
| ['entry.publish'](#entrypublish) | 当发布内容类型条目时触发。* |
entry.unpublish | 当内容类型条目被取消发布时触发。* |
media.create | 当媒体被创建时触发。 |
media.update | 当媒体被更新时触发。 |
media.delete | 当媒体被删除时触发。 |
review-workflows.updateEntryStage | 当内容在审核阶段之间移动时触发(参见 审核工作流程)。 此事件仅适用于 Strapi 的 EnterpriseThis feature is available with an Enterprise plan. 版本。 |
releases.publish | 当发布 Release 时触发(参见 Releases)。 此事件仅在 Strapi CMS 的 GrowthThis feature is available with a Growth plan. 或 EnterpriseThis feature is available with an Enterprise plan. 计划中可用。 |
*仅当此内容类型上的 draftAndPublish 已启用时。
有效载荷
🌐 Payloads
私有字段不会在有效负载中发送。
🌐 Private fields are not sent in the payload.
标题
🌐 Headers
当有效负载传递到你的 webhook 的 URL 时,它将包含特定标头:
🌐 When a payload is delivered to your webhook's URL, it will contain specific headers:
| 标题 | 描述 || --- | --- || X-Strapi-Event | 被触发的事件类型的名称。 |
entry.create
创建新条目时会触发此事件。
🌐 This event is triggered when a new entry is created.
示例有效负载
{
"event": "entry.create",
"createdAt": "2020-01-10T08:47:36.649Z",
"model": "address",
"entry": {
"id": 1,
"geolocation": {},
"city": "Paris",
"postal_code": null,
"category": null,
"full_name": "Paris",
"createdAt": "2020-01-10T08:47:36.264Z",
"updatedAt": "2020-01-10T08:47:36.264Z",
"cover": null,
"images": []
}
}
entry.update
当条目更新时会触发此事件。
🌐 This event is triggered when an entry is updated.
示例有效负载
{
"event": "entry.update",
"createdAt": "2020-01-10T08:58:26.563Z",
"model": "address",
"entry": {
"id": 1,
"geolocation": {},
"city": "Paris",
"postal_code": null,
"category": null,
"full_name": "Paris",
"createdAt": "2020-01-10T08:47:36.264Z",
"updatedAt": "2020-01-10T08:58:26.210Z",
"cover": null,
"images": []
}
}
entry.delete
当删除条目时会触发此事件。
🌐 This event is triggered when an entry is deleted.
示例有效负载
{
"event": "entry.delete",
"createdAt": "2020-01-10T08:59:35.796Z",
"model": "address",
"entry": {
"id": 1,
"geolocation": {},
"city": "Paris",
"postal_code": null,
"category": null,
"full_name": "Paris",
"createdAt": "2020-01-10T08:47:36.264Z",
"updatedAt": "2020-01-10T08:58:26.210Z",
"cover": null,
"images": []
}
}
entry.publish
发布条目时会触发此事件。
🌐 This event is triggered when an entry is published.
示例有效负载
{
"event": "entry.publish",
"createdAt": "2020-01-10T08:59:35.796Z",
"model": "address",
"entry": {
"id": 1,
"geolocation": {},
"city": "Paris",
"postal_code": null,
"category": null,
"full_name": "Paris",
"createdAt": "2020-01-10T08:47:36.264Z",
"updatedAt": "2020-01-10T08:58:26.210Z",
"publishedAt": "2020-08-29T14:20:12.134Z",
"cover": null,
"images": []
}
}
entry.unpublish
当条目未发布时会触发此事件。
🌐 This event is triggered when an entry is unpublished.
示例有效负载
{
"event": "entry.unpublish",
"createdAt": "2020-01-10T08:59:35.796Z",
"model": "address",
"entry": {
"id": 1,
"geolocation": {},
"city": "Paris",
"postal_code": null,
"category": null,
"full_name": "Paris",
"createdAt": "2020-01-10T08:47:36.264Z",
"updatedAt": "2020-01-10T08:58:26.210Z",
"publishedAt": null,
"cover": null,
"images": []
}
}
media.create
当你在条目创建时或通过媒体界面上传文件时,会触发此事件。
🌐 This event is triggered when you upload a file on entry creation or through the media interface.
示例有效负载
{
"event": "media.create",
"createdAt": "2020-01-10T10:58:41.115Z",
"media": {
"id": 1,
"name": "image.png",
"hash": "353fc98a19e44da9acf61d71b11895f9",
"sha256": "huGUaFJhmcZRHLcxeQNKblh53vtSUXYaB16WSOe0Bdc",
"ext": ".png",
"mime": "image/png",
"size": 228.19,
"url": "/uploads/353fc98a19e44da9acf61d71b11895f9.png",
"provider": "local",
"provider_metadata": null,
"createdAt": "2020-01-10T10:58:41.095Z",
"updatedAt": "2020-01-10T10:58:41.095Z",
"related": []
}
}
media.update
当你通过媒体接口更换媒体或更新媒体元数据时,会触发该事件。
🌐 This event is triggered when you replace a media or update the metadata of a media through the media interface.
示例有效负载
{
"event": "media.update",
"createdAt": "2020-01-10T10:58:41.115Z",
"media": {
"id": 1,
"name": "image.png",
"hash": "353fc98a19e44da9acf61d71b11895f9",
"sha256": "huGUaFJhmcZRHLcxeQNKblh53vtSUXYaB16WSOe0Bdc",
"ext": ".png",
"mime": "image/png",
"size": 228.19,
"url": "/uploads/353fc98a19e44da9acf61d71b11895f9.png",
"provider": "local",
"provider_metadata": null,
"createdAt": "2020-01-10T10:58:41.095Z",
"updatedAt": "2020-01-10T10:58:41.095Z",
"related": []
}
}
media.delete
仅当你通过媒体接口删除媒体时才会触发该事件。
🌐 This event is triggered only when you delete a media through the media interface.
示例有效负载
{
"event": "media.delete",
"createdAt": "2020-01-10T11:02:46.232Z",
"media": {
"id": 11,
"name": "photo.png",
"hash": "43761478513a4c47a5fd4a03178cfccb",
"sha256": "HrpDOKLFoSocilA6B0_icA9XXTSPR9heekt2SsHTZZE",
"ext": ".png",
"mime": "image/png",
"size": 4947.76,
"url": "/uploads/43761478513a4c47a5fd4a03178cfccb.png",
"provider": "local",
"provider_metadata": null,
"createdAt": "2020-01-07T19:34:32.168Z",
"updatedAt": "2020-01-07T19:34:32.168Z",
"related": []
}
}
review-workflows.updateEntryStage
EnterpriseThis feature is available with an Enterprise plan.
此事件仅适用于 Strapi 的 EnterpriseThis feature is available with an Enterprise plan. 计划。
当内容被移动到新的审核阶段时,此事件将被触发(参见 审核工作流)。
示例有效负载
{
"event": "review-workflows.updateEntryStage",
"createdAt": "2023-06-26T15:46:35.664Z",
"model": "model",
"uid": "uid",
"entity": {
"id": 2
},
"workflow": {
"id": 1,
"stages": {
"from": {
"id": 1,
"name": "Stage 1"
},
"to": {
"id": 2,
"name": "Stage 2"
}
}
}
}
releases.publish
GrowthThis feature is available with a Growth plan.
EnterpriseThis feature is available with an Enterprise plan.
当发布release时,将触发该事件。
🌐 The event is triggered when a release is published.
示例有效负载
{
"event": "releases.publish",
"createdAt": "2024-02-21T16:45:36.877Z",
"isPublished": true,
"release": {
"id": 2,
"name": "Fall Winter highlights",
"releasedAt": "2024-02-21T16:45:36.873Z",
"scheduledAt": null,
"timezone": null,
"createdAt": "2024-02-21T15:16:22.555Z",
"updatedAt": "2024-02-21T16:45:36.875Z",
"actions": {
"count": 1
}
}
}
Webhook 处理的最佳实践
🌐 Best practices for webhook handling
- 通过检查标头和有效负载签名来验证传入请求。
- 对失败的 webhook 请求实现重试以处理瞬态错误。
- 记录 webhook 事件以进行调试和监控。
- 使用安全的 HTTPS 端点接收 webhook。
- 设置速率限制以避免被多个 webhook 请求淹没。
如果你想了解更多关于如何在 Next.js 中使用 webhooks 的信息,请查看专门的博客文章。
🌐 If you want to learn more about how to use webhooks with Next.js, please have a look at the dedicated blog article.