n8n 是开源的工作流自动化工具,节点之间用连线传数据。 这篇做一个报名表单后端:Webhook 收 POST → Code 节点校验和清洗 → IF 分支 → 用 Respond to Webhook 返回 200 或 422,并用 Header Auth 挡掉没带 token 的请求。整个工作流是一份 JSON,用 CLI 导入和发布,可以放进 Git。
先看清成品
- 在 box 上真实跑通:不带 token 403,合法数据 200(邮箱已转小写、去空格),不合法 422 并列出全部错误。
- 测试中发现真实漏洞:
count: true和count: [3]都被当成合法人数(JS 里 Number(true)=1、Number([3])=3),已修复。- 还发现默认模式下 404 返回带服务器路径的 stacktrace;设
NODE_ENV=production后不再显示。
合规提醒:n8n 用 Sustainable Use License(fair-code):自用或公司内部自托管可以免费用,把它作为服务卖给别人有限制,以官方 LICENSE.md 为准。本文只用官方 npm 包和官方 CLI。接收的报名信息属于个人信息,只收必要字段,并告知用途。
运行说明:全部在 box 上真实运行(n8n 2.44.5,Node.js 24.22.1,2026-10-21 21:19–21:22,UTC+8)。box 不能用 Docker(没有守护进程权限),所以按官方 npm 方式安装。第一次用 Node 22 安装失败,npm 只报
Tracker "idealTree" already exists,看不到真正原因。npm view n8n engines显示 2.44.5 要求node >=24.0.0,而官方安装页写的是 20.19 到 24.x。换成官方 Node 24 后安装成功。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
lead_form.json |
工作流(5 个节点) |
import_and_publish.sh |
导入凭据和工作流并发布;凭据文件用完即删 |
需要准备哪些环境?
| 项目 | 说明 |
|---|---|
| Node.js | 24.x(以 npm view n8n engines 为准) |
| n8n | npm install n8n(本文 2.44.5),或按官方文档用 Docker |
| 数据目录 | N8N_USER_FOLDER 指定;里面有加密密钥,删掉后已存凭据无法解密 |
| Python | 3.9 以上 + pytest,用来写测试 |
| curl | 测试 webhook |
关键事实
| 事实 | 来源 |
|---|---|
| Webhook 节点有 Test URL 和 Production URL;正式 URL 要工作流发布(激活)后才生效 | 官方 Webhook 节点 |
| Webhook 认证可选 Basic auth、Header auth、JWT auth,也可以设置 IP 白名单 | 同上 |
responseMode: responseNode 时由 Respond to Webhook 节点决定返回内容和状态码 |
官方 Respond to Webhook |
CLI:import:workflow、publish:workflow、export:workflow;n8n 运行中改动需重启才生效(CLI 自己的提示) |
官方 CLI 文档 + 实际输出 |
文件结构
82-n8n-A-src/
├── lead_form.json
└── import_and_publish.sh
第 1 步:工作流 JSON
{
"id": "leadForm0000001",
"name": "活动报名表单校验",
"active": false,
"settings": {
"executionOrder": "v1"
},
"nodes": [
{
"id": "a1",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
0
],
"webhookId": "7b3f0d6e-1c2a-4b8e-9a51-2f6c3d1e0a01",
"parameters": {
"httpMethod": "POST",
"path": "signup",
"responseMode": "responseNode",
"options": {},
"authentication": "headerAuth"
},
"credentials": {
"httpHeaderAuth": {
"id": "signupHeaderAuth1",
"name": "报名表单 Header Auth"
}
}
},
{
"id": "a2",
"name": "校验并清洗",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
220,
0
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const b = $json.body || {};\nconst errors = [];\nconst name = String(b.name || '').trim();\nconst email = String(b.email || '').trim().toLowerCase();\nconst raw = b.count ?? 1;\n// 只接受数字或纯数字字符串;Number(true)=1、Number([3])=3 会被误判为合法\nconst count = (typeof raw === 'number' || (typeof raw === 'string' && /^\\d+$/.test(raw.trim()))) ? Number(raw) : NaN;\nif (!name) errors.push('name 必填');\nif (!/^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(email)) errors.push('email 格式不对');\nif (!Number.isInteger(count) || count < 1 || count > 5) errors.push('count 需为 1-5 的整数');\nreturn { json: { ok: errors.length === 0, errors, lead: { name, email, count, received_at: $now.toISO() } } };"
}
},
{
"id": "a3",
"name": "是否通过",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
440,
0
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.ok }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
}
},
{
"id": "a4",
"name": "回复成功",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
660,
-100
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ { status: 'ok', lead: $json.lead } }}",
"options": {
"responseCode": 200
}
}
},
{
"id": "a5",
"name": "回复错误",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
660,
100
],
"parameters": {
"respondWith": "json",
"responseBody": "={{ { status: 'invalid', errors: $json.errors } }}",
"options": {
"responseCode": 422
}
}
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "校验并清洗",
"type": "main",
"index": 0
}
]
]
},
"校验并清洗": {
"main": [
[
{
"node": "是否通过",
"type": "main",
"index": 0
}
]
]
},
"是否通过": {
"main": [
[
{
"node": "回复成功",
"type": "main",
"index": 0
}
],
[
{
"node": "回复错误",
"type": "main",
"index": 0
}
]
]
}
}
}
第 2 步:导入并发布
#!/usr/bin/env bash
# 导入 Header Auth 凭据和工作流并发布。凭据文件只在临时目录里存在,导入后立即删除。
set -euo pipefail
: "${SIGNUP_TOKEN:?先 export SIGNUP_TOKEN=\$(openssl rand -hex 24)}"
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
cat > "$tmp/cred.json" <<JSON
[{"id": "signupHeaderAuth1", "name": "报名表单 Header Auth", "type": "httpHeaderAuth",
"data": {"name": "X-Signup-Token", "value": "$SIGNUP_TOKEN"}}]
JSON
npx n8n import:credentials --input="$tmp/cred.json"
npx n8n import:workflow --input=lead_form.json
npx n8n publish:workflow --id=leadForm0000001
echo "完成。如果 n8n 正在运行,需要重启后才生效。"
第 3 步:运行
$ npm install n8n # 2.44.5,added 2342 packages in 1m
$ export N8N_USER_FOLDER=$PWD/.n8n-data TZ=Asia/Shanghai GENERIC_TIMEZONE=Asia/Shanghai
$ export SIGNUP_TOKEN=$(openssl rand -hex 24)
$ ./import_and_publish.sh
Successfully imported 1 credential.
Importing 1 workflows...
Deactivating workflow "活动报名表单校验".
Successfully imported 1 workflow.
Publishing workflow with ID: leadForm0000001 (current version)
$ NODE_ENV=production npx n8n start &
Editor is now accessible via:
http://localhost:5678
2026-10-21 21:20 UTC+8
$ curl -s -X POST http://localhost:5678/webhook/signup -H 'Content-Type: application/json' -d '{"name":"王小明","email":"xm@example.com"}' # 不带 token
Authorization data is wrong! ← HTTP 403
$ curl -s -X POST http://localhost:5678/webhook/signup -H "X-Signup-Token: $SIGNUP_TOKEN" -H 'Content-Type: application/json' -d '{"name":"王小明","email":"Xiaoming@Example.com ","count":2}'
{"status":"ok","lead":{"name":"王小明","email":"xiaoming@example.com","count":2,"received_at":"2026-10-21T21:20:36.605+08:00"}} ← HTTP 200
$ curl -s -X POST http://localhost:5678/webhook/signup -H "X-Signup-Token: $SIGNUP_TOKEN" -H 'Content-Type: application/json' -d '{"name":"韩梅梅","email":"han@example.com","count":true}'
{"status":"invalid","errors":["count 需为 1-5 的整数"]} ← HTTP 422
$ curl -s -X POST http://localhost:5678/webhook/signup -H "X-Signup-Token: $SIGNUP_TOKEN" -H 'Content-Type: application/json' -d '{"name":"李雷","email":"li@example.com","count":[3]}'
{"status":"invalid","errors":["count 需为 1-5 的整数"]} ← HTTP 422
$ curl -s -X POST http://localhost:5678/webhook/signup -H "X-Signup-Token: $SIGNUP_TOKEN" -H 'Content-Type: application/json' -d '{"name":"李雷","email":"li@example.com","count":"3"}'
{"status":"ok","lead":{"name":"李雷","email":"li@example.com","count":3,"received_at":"2026-10-21T21:20:36.829+08:00"}} ← HTTP 200
$ curl -s http://localhost:5678/webhook/signup # GET
{"code":404,"message":"This webhook is not registered for GET requests. Did you mean to make a POST request?"} ← HTTP 404
怎么检查结果?
- 用浏览器打开 http://localhost:5678 → Executions,能看到每次请求的输入和各节点输出。
- 改了 JSON 重新导入后,日志会显示
Deactivating workflow:一定要再 publish 并重启。 - 对外开放前放在 HTTPS 反向代理后面,token 定期更换。
调试示例
1. 人数校验被布尔值和数组绕过。 第一版写的是 const count = Number(b.count ?? 1),测试时:
$ curl ... -d '{"name":"韩梅梅","email":"han@example.com","count":true}'
{"status":"ok","lead":{"name":"韩梅梅","email":"han@example.com","count":1,...}} ← HTTP 200
$ curl ... -d '{"name":" ","email":"a@b.c","count":[3]}'
{"status":"invalid","errors":["name 必填"]} ← HTTP 422(count 没报错)
JavaScript 的 Number(true) 是 1,Number([3]) 是 3,都能通过 Number.isInteger。修复:只接受 number 类型或纯数字字符串,其他一律当 NaN。修复后两条都返回 422 count 需为 1-5 的整数(见运行输出)。
2. 404 把 stacktrace 返回给了调用方。 默认启动时 GET 请求得到:
{"code":404,"message":"This webhook is not registered for GET requests. Did you mean to make a POST request?","stacktrace":"ResponseError: ...\n at LiveWebhooks.findWebhook (/tmp/n8n/node_modules/n8n/src/webhooks/live-webhooks.ts:296:10)..."}
暴露了安装路径。用 NODE_ENV=production 启动后只返回 code 和 message。
3. 一开始任何人都能提交。 第一版 Webhook 没有认证。改成 Header Auth:凭据由脚本从环境变量生成临时文件导入,导入后删除;工作流 JSON 里只有凭据 id 和名称,可以放心提交(C 篇有导出检查)。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 404 webhook not registered | 工作流没发布,或改完没重启 | publish:workflow 后重启 n8n |
| 403 Authorization data is wrong! | 没带或带错 X-Signup-Token | 检查请求头 |
| 返回 200 但内容为空 | responseMode 不是 responseNode | 设成 responseNode 并接 Respond to Webhook |
| npm 报 idealTree already exists | 真实原因被掩盖,常见于 Node 版本不对 | 先 npm view n8n engines,换对版本 |
模板规则
- 工作流用 JSON 管理,用 CLI 导入发布。
- Webhook 一定加认证。
- 类型检查要严格,别依赖 Number()。
- 生产环境 NODE_ENV=production。
项目资源
| 用途 | 地址 |
|---|---|
| n8n npm 安装(官方) | https://docs.n8n.io/deploy/host-n8n/install-options/install-with-npm |
| n8n CLI 命令(官方) | https://docs.n8n.io/deploy/host-n8n/configure-n8n/use-the-command-line |
| n8n GitHub | https://github.com/n8n-io/n8n |
| n8n 许可证 | https://github.com/n8n-io/n8n/blob/master/LICENSE.md |
| n8n Webhook 节点(官方) | https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook |
| n8n Respond to Webhook(官方) | https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.respondtowebhook |
先在本机把每种错误输入都试一遍,再接到真实表单上。