网络钩子
¥Webhooks
Page summary:
Webhooks let Strapi notify external systems when content changes, while omitting the Users type for privacy. Configuration in
config/serversets default headers and endpoints to trigger third-party processing.
Webhook 是应用用来通知其他应用事件发生的构造。更准确地说,webhook 是用户定义的 HTTP 回调。使用 webhook 是告诉第三方提供商开始某些处理(CI、构建、部署...)的好方法。
¥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).
用户内容类型 webhook
¥User content-type webhooks
为了防止无意中将任何用户信息发送到其他应用,Webhooks 不适用于用户内容类型。如果需要通知其他应用有关 Users 集合中的更改,可以通过使用 ./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.conf 中设置 webhook 配置。
¥You can set webhook configurations inside the file ./config/server.
-
webhooks-
defaultHeaders:你可以设置用于 Webhook 请求的默认标头。此选项将被 webhook 本身中设置的标头覆盖。¥
defaultHeaders: You can set default headers to use for your webhook requests. This option is overwritten by the headers set in the webhook itself.
-
配置示例
¥Example configuration
- 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:
-
生成共享密钥并将其存储在环境变量中。
¥Generate a shared secret and store it in environment variables
-
让发送方对原始请求正文加上时间戳计算 HMAC(例如,SHA-256)。
¥Have the sender compute an HMAC (e.g., SHA‑256) over the raw request body plus a timestamp
-
在标头中发送签名(和时间戳)(例如,
X‑Webhook‑Signature、X‑Webhook‑Timestamp)¥Send the signature (and timestamp) in headers (e.g.,
X‑Webhook‑Signature,X‑Webhook‑Timestamp) -
收到请求后,重新计算 HMAC 并使用恒定时间检查进行比较。
¥On receipt, recompute the HMAC and compare using a constant‑time check
-
如果签名无效或时间戳过旧,无法避免重放攻击,则拒绝请求。
¥Reject if the signature is invalid or the timestamp is too old to mitigate replay
Example: Verify HMAC signatures (Node.js)
以下是一个最小化的 Node.js 中间件示例(伪代码),展示了 <ExternalLink text="HMAC" to="[https://nodejs.cn/api/crypto.html#class-hmac](https://nodejs.cn/api/crypto.html#class-hmac)" /> 的验证:
¥Here is a minimal Node.js middleware example (pseudo‑code) showing HMAC verification:
- 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 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 | 发布内容类型条目时触发。* |
entry.unpublish | 当内容类型条目取消发布时触发。* |
media.create | 创建媒体时触发。 |
media.update | 当媒体更新时触发。 |
media.delete | 删除媒体时触发。 |
review-workflows.updateEntryStage | 当内容在审阅阶段之间移动时触发(请参阅 审查工作流程)。 此事件仅适用于 EnterpriseThis feature is available with an Enterprise plan. 版本的 Strapi。 |
releases.publish | 发布版本时触发(参见 发布)。 此事件仅适用于 Strapi CMS 的 GrowthThis feature is available with a Growth plan. 或 EnterpriseThis feature is available with an Enterprise plan. 计划。 |
*仅当在此内容类型上启用 draftAndPublish 时。
¥*only when draftAndPublish is enabled on this Content Type.
有效载荷
¥Payloads
打印当前安装的 Strapi 版本。
¥Private fields and s 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.
有效负载示例
¥Example payload
{
"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.
有效负载示例
¥Example payload
{
"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.
有效负载示例
¥Example payload
{
"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.
有效负载示例
¥Example payload
{
"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.
有效负载示例
¥Example payload
{
"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.
有效负载示例
¥Example payload
{
"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.
有效负载示例
¥Example payload
{
"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.
有效负载示例
¥Example payload
{
"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. 计划。
当内容移至新的审核阶段(参见 审查工作流程)时会触发此事件。
¥This event is only available with the EnterpriseThis feature is available with an Enterprise plan. plan of Strapi.
The event is triggered when content is moved to a new review stage (see Review Workflows).
有效负载示例
¥Example payload
{
"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.
有效负载示例
¥Example payload
{
"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
-
通过检查标头和有效负载签名来验证传入请求。
¥Validate incoming requests by checking headers and payload signatures.
-
对失败的 webhook 请求实现重试以处理瞬态错误。
¥Implement retries for failed webhook requests to handle transient errors.
-
记录 webhook 事件以进行调试和监控。
¥Log webhook events for debugging and monitoring.
-
使用安全的 HTTPS 端点接收 webhook。
¥Use secure, HTTPS endpoints for receiving webhooks.
-
设置速率限制以避免被多个 webhook 请求淹没。
¥Set up rate limiting to avoid being overwhelmed by multiple webhook requests.
如果你想了解更多关于如何在 Next.js 中使用 webhook 的信息,请查看 专用博客文章。
¥If you want to learn more about how to use webhooks with Next.js, please have a look at the dedicated blog article.