快速接入
用十几分钟,把验证码接进一个注册表单:服务端创建许可,页面放入组件,服务端校验结果,最后才执行业务。
一次验证怎么走
验证服务在 https://captcha.moe,组件脚本是 https://captcha.moe/widget.js。每一次受保护的操作(一次注册、一次登录)都会走下面四步。只想看代码,可以直接跳到服务端代码。
- 你的服务端创建 intent渲染表单前调用
POST /intents,拿到 intent_id - 访客的浏览器完成验证组件凭 intent_id 完成挑战,把 token 写进表单字段
moe-captcha-response - 你的服务端兑换 token收到表单后调用
POST /siteverify,核对返回的结果 - 你的服务端执行业务兑换成功才注册、下单或发帖;每个 token 只能用一次
几个词
- Site key
mc_pub_… - 公开的站点标识,写在页面里。
- Secret key
mc_sec_… - 只放在服务端,放在
Authorization: Bearer请求头里。泄露后在控制台更换,旧密钥立即失效。 - intent(验证许可)
- 服务端为「这一次操作」申请的许可。5 分钟内有效,只能兑换一次。组件必须拿到 intent_id 才能开始验证。
- action
- 业务动作名,例如
signup、login,由你决定。创建和兑换时必须一致,这样登录页的 token 无法拿去注册。 - hostname
- 表单所在的域名,不含协议和端口,例如 example.com。必须在站点的允许域名里。
- nonce
- 创建 intent 时由服务端生成的随机值(16–128 位字母、数字、
-或_)。网络失败后重试同一次创建时复用它,不会产生第二个许可。 - token
- 访客完成验证后,组件写进表单字段
moe-captcha-response的字符串。约 2 分钟内有效。 - idempotency_key
- 兑换时的幂等键,规则和 nonce 相同。同一次提交重试时复用它,不会重复扣费;不同的提交用不同的键。
1创建站点
在控制台添加站点,填写表单所在的域名,拿到 Site key 和 Secret key。Secret key 只显示一次,请直接保存到服务端的环境变量或密钥管理里。
先在本机试,就在允许的域名里加上 localhost。去控制台添加站点
2复制服务端代码
下面是一个完整、可以直接运行的注册表单服务,只用语言自带的库。代码里的 ①–④ 对应上面的四个步骤:打开页面时创建 intent,提交时兑换 token,成功后才执行业务。
// server.mjs — Node.js 22 或更高,不需要安装依赖
// 运行:CAPTCHA_SECRET=mc_sec_你的密钥 node server.mjs,然后打开 http://localhost:3000
import { createServer } from 'node:http';
import { randomUUID } from 'node:crypto';
const API = 'https://captcha.moe';
const SECRET = process.env.CAPTCHA_SECRET;
const HOSTNAME = 'localhost'; // 必须在站点的「允许的域名」里
if (!SECRET) throw new Error('请先设置环境变量 CAPTCHA_SECRET');
// 演示用的内存存储:一次表单 → 这次验证的全部绑定信息。正式业务请放进数据库或会话。
const pending = new Map();
async function captcha(path, body) {
const res = await fetch(API + path, {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${SECRET}` },
body: JSON.stringify(body),
signal: AbortSignal.timeout(5000),
});
return { status: res.status, data: await res.json().catch(() => ({})) };
}
const form = (operation, sitekey, intent) => `<!doctype html><meta charset="utf-8">
<script src="${API}/widget.js" async defer></script>
<form method="post" action="/signup">
<input type="hidden" name="operation" value="${operation}">
<input name="email" type="email" required placeholder="you@example.com">
<div class="moe-captcha" data-sitekey="${sitekey}" data-intent="${intent}" data-endpoint="${API}"></div>
<button>注册</button>
</form>`;
function send(res, status, text, type = 'text/plain') {
res.writeHead(status, { 'content-type': `${type}; charset=utf-8`, 'cache-control': 'no-store' });
res.end(text);
}
createServer(async (req, res) => {
try {
if (req.method === 'GET' && req.url === '/') {
// ① 渲染表单之前,由服务端创建 intent,并保存这次验证的绑定信息
const op = { id: randomUUID(), nonce: randomUUID(), key: randomUUID(), action: 'signup', hostname: HOSTNAME };
const { status, data } = await captcha('/intents', { action: op.action, nonce: op.nonce, hostname: op.hostname });
if (status !== 200) return send(res, 503, `暂时无法创建验证:${data.error ?? status}`);
pending.set(op.id, { ...op, intent: data.intent_id });
return send(res, 200, form(op.id, data.sitekey, data.intent_id), 'text/html');
}
if (req.method === 'POST' && req.url === '/signup') {
let raw = '';
for await (const chunk of req) raw += chunk;
const fields = new URLSearchParams(raw);
const op = pending.get(fields.get('operation') ?? '');
const token = fields.get('moe-captcha-response'); // ② 组件写进表单的 token
if (!op || !token) return send(res, 400, '请刷新页面,重新完成验证。');
// ③ 服务端兑换 token。除了 token,其余参数都来自服务端保存的记录
const { status, data } = await captcha('/siteverify', {
response: token, intent_id: op.intent, action: op.action, hostname: op.hostname, idempotency_key: op.key,
});
if (status === 429 || status >= 500) {
// 暂时不可用:保留记录,访客重新提交会用同一个幂等键重试,不会重复扣费
return send(res, 503, '验证服务暂时不可用,请稍后重新提交。');
}
pending.delete(op.id);
if (status !== 200 || data.success !== true || data.intent_id !== op.intent ||
data.action !== op.action || data.hostname !== op.hostname) {
// token 过期、已用或不匹配:重新验证(刷新页面会创建新的 intent)
return send(res, 400, `验证未通过(${data.error ?? '结果不匹配'}),请刷新页面重新验证。`);
}
// ④ 验证通过后才执行业务。这里只回显邮箱
return send(res, 200, `注册成功:${fields.get('email')}`);
}
send(res, 404, '');
} catch {
send(res, 503, '验证服务暂时不可用,请稍后重新提交。'); // 网络错误或超时:可以原样重试
}
}).listen(3000, () => console.log('打开 http://localhost:3000'));
其他语言照同样的两次 HTTP 调用即可,参见服务端 API。需要持久会话、并发重试和业务幂等的完整写法,见完整表单示例和接入细节与进阶。
3页面里的组件
上面的服务端代码已经生成了这段 HTML。接入你自己的页面时,把组件放进 form 里,由服务端模板填入 sitekey 和 intent_id:
<script src="https://captcha.moe/widget.js" async defer></script>
<form method="post" action="/signup">
<input name="email" type="email" required>
<!-- sitekey 和 intent_id 由你的服务端在渲染这个页面时填入 -->
<div class="moe-captcha"
data-sitekey="{{ sitekey }}"
data-intent="{{ intent_id }}"
data-endpoint="https://captcha.moe"></div>
<button type="submit">注册</button>
</form>访客完成验证后,组件在表单里写入隐藏字段 moe-captcha-response。组件显示「已验证」只代表浏览器拿到了 token,是否放行由你的服务端兑换后决定。模式、外观和回调见组件配置。
4运行并检查
export CAPTCHA_SECRET=mc_sec_你的密钥
node server.mjs
# 打开 http://localhost:3000打开页面后逐项确认:
- 正常完成验证后,提交成功。
- 不完成验证直接提交、或把同一个 token 再提交一次,都会被拒绝。
- 在控制台的站点数据里能看到「校验成功」次数增加。
用 curl 试接口
不写代码,也可以先看看两次调用分别返回什么:
# ① 创建 intent(在你的服务端执行,Secret 不能出现在浏览器里)
curl -sS https://captcha.moe/intents \
-H "Authorization: Bearer $CAPTCHA_SECRET" \
-H "Content-Type: application/json" \
-d '{"action":"signup","nonce":"'"$(uuidgen)"'","hostname":"localhost"}'
# 返回:
# {"intent_id":"8c1f…","sitekey":"mc_pub_…","action":"signup","hostname":"localhost","expires_at":1789400000}失败时怎么办
错误响应都是 {"error":"错误码"}。先看是哪一类,再决定是让访客重新验证,还是原样重试:
| 情况 | 你会看到 | 该怎么做 |
|---|---|---|
| token 过期、已用,或和这次提交对不上 | 400 / 409:invalid-or-expired-permit、invalid-input-response、already-redeemed | 让访客重新验证:创建新的 intent,重新渲染组件 |
| 密钥或域名配置错误 | 401 invalid-input-secret、403 origin-not-allowed | 修正配置,重试不会成功 |
| 本月额度用完且余额不足 | 402 insufficient-balance | 充值后用同一个请求重试 |
| 服务暂时不可用、限流、网络超时 | 503 unavailable、429 rate-limited、超时 | 保留这次提交,用相同的参数和幂等键重试;得到结果前不要执行业务 |
所有错误码见错误码与排查。
SDK 与框架
SDK 会替你处理请求、超时和结果核对。包名已经确定,发布到各平台前,请从仓库对应目录安装,或者直接使用上面的 HTTP 写法(不需要 SDK)。Node.js 服务端 SDK 需要 Node.js 22 或更高;前端框架包需要 Node.js 22.22+ 或 24.15+。
| 用途 | 包名 | 安装命令 | 源码目录 |
|---|---|---|---|
| Node.js 服务端 | @captcha-moe/server | npm install @captcha-moe/server即将发布 | sdks/node |
| Python 服务端 | captchamoe-server | pip install captchamoe-server即将发布 | sdks/python |
| PHP 服务端 | captchamoe/server | composer require captchamoe/server即将发布 | sdks/php |
| Go 服务端 | captcha.moe/sdk | go get captcha.moe/sdk即将发布 | sdks/go |
| Rust 服务端 | captchamoe-server | cargo add captchamoe-server即将发布 | sdks/rust |
| 前端框架(React、Vue、Svelte 等) | @captcha-moe/frontend | npm install @captcha-moe/frontend即将发布 | integrations/frontend |
| Django | captchamoe-django | pip install captchamoe-django即将发布 | integrations/django |
| Laravel | captchamoe/laravel | composer require captchamoe/laravel即将发布 | integrations/laravel |
| Rails | captchamoe-rails | gem 'captchamoe-rails'即将发布 | integrations/rails |
| WordPress | CaptchaMoe 插件 | 上传插件 zip 并启用即将发布 | integrations/wordpress |
请求失败时,SDK 抛出的错误会带上接口返回的错误码和出错字段,可以直接对照错误码处理:
| SDK | 错误码 | 出错字段 |
|---|---|---|
| Node.js | error.code | error.field |
| Python | error.code | error.field |
| Go | err.Code | err.Field |
| PHP | $e->apiCode | $e->field |
| Rust | Error::Api { status, code, field },或 code() / status() | field |
本地开发
本地开发直接使用线上组件 https://captcha.moe/widget.js,不需要自己运行验证服务:
- 在站点的允许域名里加入
localhost,创建 intent 时 hostname 填localhost。 - 用
http://localhost:端口打开页面。如果用127.0.0.1打开,要把127.0.0.1也加入允许域名,并用它作为 hostname。 - 测试站点和正式站点分开建,避免测试流量混进正式数据。
自己部署验证服务时,把文中的 https://captcha.moe 换成你的服务地址。