Cloudflare Turnstile 人机验证集成文档

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 - 添加繁体中文翻译

新增翻译键:

  • 请完成人机验证
  • 人机验证失败,请重试

工作流程

  1. 用户访问登录页面
  2. 用户输入邮箱和密码
  3. 用户完成 Cloudflare Turnstile 验证
  4. 验证成功后,启用登录按钮
  5. 用户点击登录按钮
  6. 前端发送登录请求,包含 turnstileToken
  7. 后端验证 Turnstile token 的有效性
  8. 验证通过后继续正常的登录流程

安全特性

  • 登录按钮在 Turnstile 验证完成前处于禁用状态
  • 登录失败时会自动重置 Turnstile 验证
  • Token 过期时会自动清除状态
  • 后端双重验证 Turnstile token

注意事项

  • 站点密钥 (Site Key) 是公开的,可以在前端使用
  • 密钥 (Secret Key) 必须保密,仅在服务端使用
  • Turnstile token 有效期为 5 分钟
  • 如果验证失败,用户需要重新完成验证

通用集成指南

集成步骤

1. 获取密钥

  1. 访问 Cloudflare Dashboard
  2. 进入 Turnstile 页面
  3. 添加新站点,获取:
    • 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