CTPRooF 为登录、注册、提交、投票、领券等表单场景提供免交互的人机验证。前端完成工作量证明拿到一次性票据,服务端核销票据后才放行——脚本批量请求与票据重放都会被拒绝。
不弹拼图、不要求识图,正常用户无感通过;难度自适应,弱设备同样可用。
每次验证签发独立 ticket,核销一次即失效,重放会返回 timeout-or-duplicate。
前端结果不可信。是否放行以接入方服务端调用 siteverify 的返回值为准。
前端引入脚本接管表单,服务端核销票据。整套流程约 10 行代码。
<script src="https://www.chingtsi.com/ctproof.js" data-ctproof-site="你的 site_key" defer></script>
<form action="/login" method="post" data-ctproof>
<!-- 原有字段保持不变 -->
<div data-ctproof-state></div>
</form>
脚本接管所有带 data-ctproof 的表单:提交时自动完成挑战计算,成功后在表单内注入隐藏字段 ctproof-ticket 并继续提交;失败则停留在当前页并提示。[data-ctproof-state] 用于展示「正在验证 / 验证通过 / 失败」。
<?php
$ticket = trim((string)($_POST['ctproof-ticket'] ?? ''));
$res = file_get_contents('https://www.chingtsi.com/ctproof-api?action=siteverify', false, stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
'content' => http_build_query([
'site_key' => '你的 site_key',
'secret' => '你的 secret',
'ticket' => $ticket,
]),
'timeout' => 5,
],
]));
$r = json_decode((string)$res, true);
if (empty($r['success'])) {
exit('人机验证未通过:' . ($r['error'] ?? 'unknown'));
}
// 通过,继续业务逻辑const res = await fetch('https://www.chingtsi.com/ctproof-api?action=siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
site_key: '你的 site_key',
secret: '你的 secret',
ticket: req.body['ctproof-ticket'] || '',
}),
});
const r = await res.json();
if (!r.success) return res.status(400).send('人机验证未通过:' + (r.error || 'unknown'));
// 通过,继续业务逻辑import requests
r = requests.post('https://www.chingtsi.com/ctproof-api?action=siteverify', data={
'site_key': '你的 site_key',
'secret': '你的 secret',
'ticket': request.form.get('ctproof-ticket', ''),
}, timeout=5).json()
if not r.get('success'):
abort(400, '人机验证未通过:' + str(r.get('error')))
# 通过,继续业务逻辑curl -s -X POST 'https://www.chingtsi.com/ctproof-api?action=siteverify' \
-d 'site_key=你的 site_key' \
-d 'secret=你的 secret' \
-d 'ticket=前端拿到的 ticket'若你的客户端不是浏览器页面(小程序、App、服务端渲染),可直接走三步接口。挑战与票据均由服务端签发,走过期时间。
# 1) 取挑战(GET,需带 Origin 或 Referer 以校验来源域名)
GET /ctproof-api?action=challenge&site_key=你的site_key
→ {"ok":true,"cid":"...","nonce":"...","difficulty":3,"expires":1767...,"sig":"...","ttl":120}
# 2) 计算工作量证明:找整数 n,使 sha256(nonce + ":" + n) 的十六进制前缀含 difficulty 个 0
POST /ctproof-api?action=answer
site_key, cid, nonce, difficulty, expires, sig, answer=n
→ {"ok":true,"ticket":"32位十六进制","ttl":180}
# 3) 服务端核销(一次性)
POST /ctproof-api?action=siteverify
site_key, secret, ticket
→ {"ok":true,"success":true,"error-codes":[],"challenge_ts":"...","hostname":"你的域名","site_key":"..."}
挑战带 HMAC 签名,参数被篡改会被拒绝;同一挑战不能重复作答;票据核销一次即失效。默认工作量难度 3(约数千次哈希,通常 100ms 内完成)。
申请通过后,后台会为站点签发 site_key 与 secret。为防止他人冒用你的域名接入,后台可对站点做一次归属校验,三选一即可,通过后站点标记为「已校验」。
| 方式 | 需要做什么 | 校验依据 |
|---|---|---|
| DNS TXT | 在域名 DNS 添加一条 TXT 记录:_ctproof-verify.你的域名 → ctp-site-key=你的site_key |
服务器直接查询 DNS 记录,不依赖站点能否被抓取,最稳 |
| well-known | 在站点根目录放置 /.well-known/ctproof.txt,内容包含你的 site_key |
服务器抓取该文件并与 site_key 比对 |
| meta | 在首页 <head> 加 <meta name="ctproof-site" content="你的site_key"> |
服务器抓取首页并匹配 meta 内容 |
校验只影响后台的「已校验」标记,不阻断接口调用;域名白名单以申请时登记的 site_url 为准,来源域名不一致会返回 domain-mismatch。若站点未启用 HTTPS 或服务器出网受限,请优先使用 DNS TXT 方式。
| error-code | 含义 | 处理建议 |
|---|---|---|
| missing-input-secret | 未提交 secret | 检查服务端参数 |
| invalid-input-secret | secret 与站点不匹配(可能已被重置) | 在后台重置后同步新 secret |
| missing-input-response | 未提交 ticket | 确认前端注入成功 |
| invalid-input-response | 票据不存在或格式错误 | 票据需原样透传 |
| timeout-or-duplicate | 票据已过期或已被核销 | 不可重试同一票据,需重新验证 |
| invalid-input-sitekey | site_key 不存在 | 核对 site_key |
| domain-mismatch | 调用来源域名与登记域名不一致 | 检查页面域名白名单 |
| bad-proof | 工作量证明不成立 | 按 difficulty 重新计算 |
| replay | 同一挑战重复作答 | 每次重新取挑战 |
| site-disabled | 该站点已被管理员停用 | 联系管理员 |
| rate-limited | 请求过于频繁 | 退避重试 |
下面这段是真实调用:点击后前端完成一次验证拿到票据,再提交到本站服务端核销。连续点击第二次会看到「票据已被使用」——这就是防重放。
演示站点:ctp_pub_DEMO0000000000000001,登记域名 chingtsi.com。生产接入请先申请,获得独立的 site_key 与 secret。
提交站点与应用场景,审核通过后发放 site_key 与 secret。