Cloudflare Turnstile 人机验证集成文档
Cloudflare Turnstile 人机验证集成文档
什么是 Cloudflare Turnstile
Cloudflare Turnstile 是一种隐私友好的 CAPTCHA 替代方案,用于区分人类用户和机器人。与 reCAPTCHA 不同,Turnstile 不会收集用户数据,提供更好的隐私保护。
工作原理
┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 用户访问 │────▶│ 加载 Turnstile │────▶│ 用户完成验证 │
│ 登录页面 │ │ 验证组件 │ │ (无感知/点击) │
└─────────────┘ └─────────────────┘ └────────┬────────┘
│
┌─────────────────┐ │
│ 后端验证 token │◀───────────┘
│ 通过 Cloudflare │
│ API 验证 │
└────────┬────────┘
│
┌────────▼────────┐
│ 验证成功/失败 │
│ 继续登录流程 │
└─────────────────┘
配置信息
密钥配置
⚠️ 安全提示: 密钥已移至环境变量,请勿在代码中硬编码!
- 站点密钥 (Site Key): 从 Cloudflare Dashboard 获取,用于前端
- 密钥 (Secret Key): 从 Cloudflare Dashboard 获取,仅用于后端
配置文件
在 next.config.ts 中添加了以下环境变量:
// Cloudflare Turnstile 验证配置 (从环境变量读取)
NEXT_PUBLIC_TURNSTILE_SITE_KEY: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || "",
TURNSTILE_SECRET_KEY: process.env.TURNSTILE_SECRET_KEY || "",
或在 .env 文件中配置:
# .env.local
NEXT_PUBLIC_TURNSTILE_SITE_KEY=your_site_key_here
TURNSTILE_SECRET_KEY=your_secret_key_here
安装依赖
pnpm add @marsidev/react-turnstile
修改的文件
1. 桌面端登录表单
文件: src/app/[shopId]/login/components/form.tsx
1.1 添加 Turnstile 组件导入
import { useState, useRef } from 'react'
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile';
interface LoginData {
email: string;
password: string;
shopId: string;
turnstileToken: string; // 新增
}
1.2 添加 turnstileToken 状态管理
export default function LoginForm({ lang, shopId }: { lang: string, shopId: string }) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [turnstileToken, setTurnstileToken] = useState('') // 新增
const turnstileRef = useRef<TurnstileInstance>(null) // 新增
// ...
}
1.3 在表单中添加 Turnstile 验证组件
<form onSubmit={handleSubmit} className="space-y-6">
{/* 原有的邮箱、密码输入框 */}
{/* Cloudflare Turnstile 验证 */}
<div className="flex justify-center">
<Turnstile
ref={turnstileRef}
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || ''}
onSuccess={(token) => setTurnstileToken(token)}
onError={() => setTurnstileToken('')}
onExpire={() => setTurnstileToken('')}
/>
</div>
{/* 提交按钮 */}
<button
type="submit"
disabled={isMutating || !turnstileToken} // 未验证时禁用
>
登录
</button>
</form>
1.4 提交时验证 token 是否存在
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!turnstileToken) {
toast.error(getTranslation(lang, '请完成人机验证'))
return
}
toast.promise(login({ email, password, shopId, turnstileToken }), {
loading: getTranslation(lang, '登录中'),
success: getTranslation(lang, '登录成功'),
error: (err) => err.message || getTranslation(lang, '登录失败')
});
}
1.5 登录失败时重置 Turnstile
const { trigger: login, isMutating } = useSWRMutation('/api/login', fetcher<LoginData>, {
onSuccess(data) {
toBack();
},
onError(err) {
// 登录失败时重置 Turnstile
turnstileRef.current?.reset()
setTurnstileToken('')
}
})
2. 移动端登录表单
文件: src/app/[shopId]/app/login/components/index.tsx
2.1 添加 Turnstile 组件导入
import { useState, useRef } from 'react';
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile';
interface LoginData {
email: string;
password: string;
shopId: string;
turnstileToken: string; // 新增
}
2.2 添加 turnstileToken 状态管理
export default function MobileLoginForm({ lang, shopId }: LoginProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [turnstileToken, setTurnstileToken] = useState(''); // 新增
const turnstileRef = useRef<TurnstileInstance>(null); // 新增
// ...
}
2.3 在表单中添加 Turnstile 验证组件
<form onSubmit={handleSubmit} className="bg-white rounded-lg shadow-md p-6 space-y-4">
{/* 原有的邮箱、密码输入框 */}
{/* Cloudflare Turnstile 验证 */}
<div className="flex justify-center py-2">
<Turnstile
ref={turnstileRef}
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || ''}
onSuccess={(token) => setTurnstileToken(token)}
onError={() => setTurnstileToken('')}
onExpire={() => setTurnstileToken('')}
/>
</div>
{/* 提交按钮 */}
<button
type="submit"
disabled={isMutating || !turnstileToken} // 未验证时禁用
>
登录
</button>
</form>
2.4 提交时验证 token 是否存在
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!turnstileToken) {
toast.error(getTranslation(lang, '请完成人机验证'))
return
}
toast.promise(login({ email, password, shopId, turnstileToken }), {
loading: getTranslation(lang, '登录中'),
success: getTranslation(lang, '登录成功'),
error: (err) => err.message || getTranslation(lang, '登录失败')
});
}
2.5 登录失败时重置 Turnstile
const { trigger: login, isMutating } = useSWRMutation('/api/login', fetcher<LoginData>, {
onSuccess() {
window.location.href = redirect
},
onError() {
// 登录失败时重置 Turnstile
turnstileRef.current?.reset();
setTurnstileToken('');
}
})
3. 登录 API
文件: src/app/api/login/route.ts
3.1 添加 turnstileToken 到请求类型定义
export type INFO = {
email: string;
password: string;
shopId: string;
turnstileToken: string; // 新增
}
3.2 添加 verifyTurnstileToken 函数验证 token
// 验证 Turnstile token
async function verifyTurnstileToken(token: string): Promise<boolean> {
try {
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
secret: process.env.TURNSTILE_SECRET_KEY, // 服务端密钥
response: token, // 前端传来的 token
}),
});
const data = await response.json();
return data.success === true;
} catch (err) {
console.error('Turnstile verification error:', err);
return false;
}
}
3.3 在登录逻辑中验证 Turnstile token
export async function POST(request: Request) {
const cookiesList = await cookies()
const lang = cookiesList.get('lang')?.value || 'zh';
try {
const { email, password, shopId, turnstileToken } = await request.json();
// 验证必填字段
if (!email || !password) {
return error(getTranslation(lang, '请输入电子邮件和密码'))
}
// 验证 Turnstile token
if (!turnstileToken) {
return error(getTranslation(lang, '请完成人机验证'));
}
const isTurnstileValid = await verifyTurnstileToken(turnstileToken);
if (!isTurnstileValid) {
return error(getTranslation(lang, '人机验证失败,请重试'));
}
// 继续原有的登录验证逻辑...
// 验证用户、密码等
}
}
4. 翻译文件
src/locales/zh.json- 添加中文翻译src/locales/en.json- 添加英文翻译src/locales/tw.json- 添加繁体中文翻译
新增翻译键:
请完成人机验证人机验证失败,请重试
工作流程
- 用户访问登录页面
- 用户输入邮箱和密码
- 用户完成 Cloudflare Turnstile 验证
- 验证成功后,启用登录按钮
- 用户点击登录按钮
- 前端发送登录请求,包含
turnstileToken - 后端验证 Turnstile token 的有效性
- 验证通过后继续正常的登录流程
安全特性
- 登录按钮在 Turnstile 验证完成前处于禁用状态
- 登录失败时会自动重置 Turnstile 验证
- Token 过期时会自动清除状态
- 后端双重验证 Turnstile token
注意事项
- 站点密钥 (Site Key) 是公开的,可以在前端使用
- 密钥 (Secret Key) 必须保密,仅在服务端使用
- Turnstile token 有效期为 5 分钟
- 如果验证失败,用户需要重新完成验证
通用集成指南
集成步骤
1. 获取密钥
- 访问 Cloudflare Dashboard
- 进入 Turnstile 页面
- 添加新站点,获取:
- Site Key (站点密钥) - 前端使用
- Secret Key (密钥) - 后端使用
2. 前端集成
安装依赖
# React 项目
npm install @marsidev/react-turnstile
# 或其他框架使用官方 JS 库
npm install turnstile
React 示例
import { useState, useRef } from 'react';
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile';
function LoginForm() {
const [turnstileToken, setTurnstileToken] = useState('');
const turnstileRef = useRef<TurnstileInstance>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!turnstileToken) {
alert('请完成人机验证');
return;
}
// 发送登录请求,包含 turnstileToken
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
password,
turnstileToken
})
});
if (!response.ok) {
// 登录失败,重置 Turnstile
turnstileRef.current?.reset();
setTurnstileToken('');
}
};
return (
<form onSubmit={handleSubmit}>
{/* 其他表单字段 */}
<Turnstile
ref={turnstileRef}
siteKey="YOUR_SITE_KEY"
onSuccess={(token) => setTurnstileToken(token)}
onError={() => setTurnstileToken('')}
onExpire={() => setTurnstileToken('')}
/>
<button type="submit" disabled={!turnstileToken}>
登录
</button>
</form>
);
}
纯 HTML/JS 示例
<!-- 在 head 中引入 -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<!-- 在表单中添加 -->
<form id="login-form">
<input type="email" name="email" required />
<input type="password" name="password" required />
<!-- Turnstile 容器 -->
<div class="cf-turnstile"
data-sitekey="YOUR_SITE_KEY"
data-callback="onTurnstileSuccess"
data-error-callback="onTurnstileError">
</div>
<button type="submit" id="submit-btn" disabled>登录</button>
</form>
<script>
let turnstileToken = '';
function onTurnstileSuccess(token) {
turnstileToken = token;
document.getElementById('submit-btn').disabled = false;
}
function onTurnstileError() {
turnstileToken = '';
document.getElementById('submit-btn').disabled = true;
}
document.getElementById('login-form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: formData.get('email'),
password: formData.get('password'),
turnstileToken
})
});
if (!response.ok) {
// 重置 Turnstile
turnstile.reset();
turnstileToken = '';
}
});
</script>
3. 后端集成
Node.js / Next.js 示例
// 验证 Turnstile token 的函数
async function verifyTurnstileToken(token: string): Promise<boolean> {
try {
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
secret: process.env.TURNSTILE_SECRET_KEY, // 从环境变量读取
response: token,
}),
});
const data = await response.json();
return data.success === true;
} catch (err) {
console.error('Turnstile verification error:', err);
return false;
}
}
// 登录 API 路由
export async function POST(request: Request) {
const { email, password, turnstileToken } = await request.json();
// 1. 验证 Turnstile token
if (!turnstileToken) {
return new Response(JSON.stringify({ error: '请完成人机验证' }), {
status: 400
});
}
const isTurnstileValid = await verifyTurnstileToken(turnstileToken);
if (!isTurnstileValid) {
return new Response(JSON.stringify({ error: '人机验证失败' }), {
status: 400
});
}
// 2. 继续正常的登录验证逻辑
// ...
}
Python / Flask 示例
import requests
from flask import Flask, request, jsonify
import os
app = Flask(__name__)
def verify_turnstile_token(token: str) -> bool:
"""验证 Turnstile token"""
try:
response = requests.post(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
json={
'secret': os.environ.get('TURNSTILE_SECRET_KEY'),
'response': token
}
)
data = response.json()
return data.get('success', False)
except Exception as e:
print(f'Turnstile verification error: {e}')
return False
@app.route('/api/login', methods=['POST'])
def login():
data = request.get_json()
email = data.get('email')
password = data.get('password')
turnstile_token = data.get('turnstileToken')
# 验证 Turnstile
if not turnstile_token:
return jsonify({'error': '请完成人机验证'}), 400
if not verify_turnstile_token(turnstile_token):
return jsonify({'error': '人机验证失败'}), 400
# 继续登录逻辑
# ...
if __name__ == '__main__':
app.run()
PHP 示例
<?php
function verifyTurnstileToken($token) {
$secret = $_ENV['TURNSTILE_SECRET_KEY'];
$data = [
'secret' => $secret,
'response' => $token
];
$options = [
'http' => [
'header' => "Content-Type: application/json\r\n",
'method' => 'POST',
'content' => json_encode($data)
]
];
$context = stream_context_create($options);
$result = file_get_contents(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
false,
$context
);
$response = json_decode($result, true);
return $response['success'] ?? false;
}
// 登录处理
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
$turnstileToken = $data['turnstileToken'] ?? '';
if (!$turnstileToken) {
http_response_code(400);
echo json_encode(['error' => '请完成人机验证']);
exit;
}
if (!verifyTurnstileToken($turnstileToken)) {
http_response_code(400);
echo json_encode(['error' => '人机验证失败']);
exit;
}
// 继续登录逻辑
// ...
}
?>
Turnstile 配置选项
前端配置
| 属性 | 说明 | 可选值 |
|---|---|---|
data-sitekey | 站点密钥 | 你的 Site Key |
data-theme | 主题 | light, dark, auto |
data-size | 尺寸 | normal, compact, flexible |
data-language | 语言 | zh-CN, en, ja 等 |
data-callback | 成功回调 | 函数名 |
data-error-callback | 错误回调 | 函数名 |
data-expired-callback | 过期回调 | 函数名 |
主题示例
<!-- 暗色主题 -->
<div class="cf-turnstile"
data-sitekey="YOUR_SITE_KEY"
data-theme="dark">
</div>
<!-- 紧凑尺寸 -->
<div class="cf-turnstile"
data-sitekey="YOUR_SITE_KEY"
data-size="compact">
</div>
安全最佳实践
1. 密钥管理
- Site Key: 可以公开,用于前端
- Secret Key: 必须保密,仅用于后端,存储在环境变量中
2. 验证流程
前端显示 Turnstile ──▶ 用户完成验证 ──▶ 获取 token
│
▼
后端接收请求 ──▶ 验证 token ──▶ 验证通过后才处理业务逻辑
3. 错误处理
- Token 有效期为 5 分钟
- 登录失败后必须重置 Turnstile
- 网络错误时给用户明确提示
4. 防护措施
- 登录按钮在验证完成前禁用
- 后端必须二次验证 token
- 防止重放攻击(token 一次性使用)
常见问题
Q: Turnstile 和 reCAPTCHA 有什么区别?
A: Turnstile 更注重隐私保护,不会收集用户数据,且通常提供更好的用户体验(往往是无感知的)。
Q: 是否需要用户点击"我不是机器人"?
A: 通常不需要。Turnstile 使用多种信号在后台判断,大多数情况下用户无需任何操作。
Q: 在开发环境如何测试?
A: Cloudflare 提供测试密钥:
- Site Key:
1x00000000000000000000AA - Secret Key:
1x0000000000000000000000000000000AA
Q: 移动端如何适配?
A: 使用 data-size="compact" 或 data-size="flexible" 适应小屏幕。
参考资源
本文由萧兮的博客原创发布,欢迎转载,转载务必保留原文链接。
萧兮的博客:https://www.20010515.xyz · 原文:https://www.20010515.xyz/posts/84a07f68-57ab-4b6f-876e-40ea1c950075