快速接入自托管人机验证服务
开源 · 自托管 · 无追踪在 HTML 中引入 cap-widget 并使用
在后端验证 cap-widget 返回的 token
<!-- 引入 TokenGX CAPTCHA(品牌版) -->
<script src="https://cap.tokengx.cn/cap-widget.js"></script>
<!-- 或使用官方 CDN 版本 -->
<!-- <script src="https://cdn.jsdelivr.net/npm/cap-widget"></script> -->
<!-- 在表单中使用 -->
<form id="myForm">
<input type="tel" name="phone" placeholder="手机号码">
<input type="password" name="password" placeholder="密码">
<!-- CAPTCHA 组件 -->
<cap-widget data-cap-api-endpoint="https://cap.tokengx.cn/YOUR_SITE_KEY/"></cap-widget>
<button type="submit">登录</button>
</form>
<script>
const widget = document.querySelector('cap-widget');
let captchaToken = null;
// 监听验证成功事件
widget.addEventListener('solve', (e) => {
captchaToken = e.detail.token;
console.log('验证通过');
});
// 监听验证失败事件
widget.addEventListener('error', (e) => {
captchaToken = null;
console.error('验证失败:', e.detail);
});
// 表单提交时携带 token
document.getElementById('myForm').addEventListener('submit', async (e) => {
e.preventDefault();
if (!captchaToken) {
alert('请先完成人机验证');
return;
}
const formData = new FormData(e.target);
formData.append('captchaToken', captchaToken);
// 发送到后端
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phone: formData.get('phone'),
password: formData.get('password'),
captchaToken: captchaToken
})
});
const result = await response.json();
if (result.success) {
window.location.href = '/dashboard';
} else {
alert(result.error || '登录失败');
}
});
</script>
npm install cap-widget
import { useEffect, useRef, useState } from 'react';
import 'cap-widget';
export default function LoginForm() {
const widgetRef = useRef(null);
const [captchaToken, setCaptchaToken] = useState(null);
useEffect(() => {
const widget = widgetRef.current;
if (!widget) return;
const handleSolve = (e) => {
setCaptchaToken(e.detail.token);
};
const handleError = () => {
setCaptchaToken(null);
};
widget.addEventListener('solve', handleSolve);
widget.addEventListener('error', handleError);
return () => {
widget.removeEventListener('solve', handleSolve);
widget.removeEventListener('error', handleError);
};
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
if (!captchaToken) {
alert('请先完成人机验证');
return;
}
// 提交逻辑...
};
return (
<form onSubmit={handleSubmit}>
<input type="tel" name="phone" placeholder="手机号码" />
<input type="password" name="password" placeholder="密码" />
<cap-widget
ref={widgetRef}
data-cap-api-endpoint="https://cap.tokengx.cn/YOUR_SITE_KEY/"
/>
<button type="submit">登录</button>
</form>
);
}
<template>
<form @submit.prevent="handleSubmit">
<input v-model="phone" type="tel" placeholder="手机号码">
<input v-model="password" type="password" placeholder="密码">
<cap-widget
ref="widget"
:data-cap-api-endpoint="`https://cap.tokengx.cn/${siteKey}/`"
@solve="onSolve"
@error="onError"
/>
<button type="submit">登录</button>
</form>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import 'cap-widget';
const siteKey = 'YOUR_SITE_KEY';
const widget = ref(null);
const captchaToken = ref(null);
const phone = ref('');
const password = ref('');
const onSolve = (e) => {
captchaToken.value = e.detail.token;
};
const onError = () => {
captchaToken.value = null;
};
const handleSubmit = async () => {
if (!captchaToken.value) {
alert('请先完成人机验证');
return;
}
// 提交逻辑...
};
</script>
data-cap-api-endpoint 的格式为:
https://cap.tokengx.cn/{SiteKey}/
注意末尾需要有斜杠 /。
const express = require('express');
const app = express();
const CAPTCHA_SECRET_KEY = process.env.CAPTCHA_SECRET_KEY;
async function verifyCaptcha(token) {
try {
const response = await fetch('https://cap.tokengx.cn/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
response: token,
secret: CAPTCHA_SECRET_KEY
})
});
const data = await response.json();
return data.success === true;
} catch (error) {
console.error('Captcha verification error:', error);
return false;
}
}
app.post('/api/login', async (req, res) => {
const { phone, password, captchaToken } = req.body;
// 验证 CAPTCHA
if (!captchaToken || !(await verifyCaptcha(captchaToken))) {
return res.status(400).json({
success: false,
error: '人机验证失败,请重试'
});
}
// 验证通过,处理登录逻辑
// ...
});
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
CAPTCHA_SECRET_KEY = 'sk-your-secret-key'
def verify_captcha(token):
try:
response = requests.post(
'https://cap.tokengx.cn/siteverify',
json={
'response': token,
'secret': CAPTCHA_SECRET_KEY
}
)
data = response.json()
return data.get('success', False)
except Exception as e:
print(f'Captcha verification error: {e}')
return False
@app.route('/api/login', methods=['POST'])
def login():
data = request.json
token = data.get('captchaToken')
# 验证 CAPTCHA
if not token or not verify_captcha(token):
return jsonify({
'success': False,
'error': '人机验证失败,请重试'
}), 400
# 验证通过,处理登录逻辑
# ...
return jsonify({'success': True})
<?php
function verifyCaptcha($token) {
$secretKey = 'sk-your-secret-key';
$response = file_get_contents('https://cap.tokengx.cn/siteverify', false, stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/json',
'content' => json_encode([
'response' => $token,
'secret' => $secretKey
])
]
]));
$data = json_decode($response, true);
return $data['success'] === true;
}
// 在登录处理中使用
$captchaToken = $_POST['captchaToken'] ?? '';
if (!$captchaToken || !verifyCaptcha($captchaToken)) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => '人机验证失败,请重试'
]);
exit;
}
// 验证通过,处理登录逻辑
// ...
?>
# .env
CAPTCHA_SECRET_KEY=sk-your-secret-key
NEXT_PUBLIC_CAPTCHA_SITE_KEY=your-site-key
| 事件名 | 说明 | 回调参数 |
|---|---|---|
solve |
验证成功 | e.detail.token - 三段式 token |
error |
验证失败 | e.detail.code - 错误码 |
progress |
验证进度 | e.detail.progress - 进度百分比 |
reset |
重置验证 | 无 |
| 属性名 | 说明 | 示例值 |
|---|---|---|
data-cap-api-endpoint |
API 端点(必填) | https://cap.tokengx.cn/YOUR_SITE_KEY/ |
data-cap-lang |
语言 | zh、en、ja 等 |
data-cap-hidden-field-name |
隐藏表单字段名 | captcha_token |
data-cap-worker-count |
Worker 数量 | 4 |
POST
https://cap.tokengx.cn/siteverify
{
"response": "siteKey:challenge:signature",
"secret": "sk-your-secret-key"
}
{
"success": true
}
{
"success": false,
"error": "Invalid token"
}
response 参数必须是三段式格式:
siteKey:challenge:signature
这是 Cap Widget 自动生成的格式,直接传递即可。
cap-widget {
--cap-widget-width: 280px;
--cap-widget-height: 60px;
--cap-background: #ffffff;
--cap-border-color: #e0e0e0;
--cap-color: #333333;
--cap-border-radius: 12px;
--cap-checkbox-size: 20px;
--cap-checkbox-background: #f0f0f0;
--cap-checkbox-border: #ccc;
--cap-checkmark: #000;
}
1. 检查 data-cap-api-endpoint 格式是否正确(末尾需有 /)
2. 检查浏览器控制台是否有错误
3. 确认 CSP 配置允许加载 cap.tokengx.cn 的资源
1. 检查 Secret Key 是否正确
2. 确认 token 是三段式格式
3. 检查网络连接是否正常
const widget = document.querySelector('cap-widget');
widget.reset();
- Chrome 60+
- Firefox 60+
- Safari 12+
- Edge 79+