在 Next.js 中无缝集成 Cloudflare Turnstile - 打造更安全的网站

详细介绍如何在 Next.js 14 项目中集成 Cloudflare Turnstile,提升网站安全性,改善用户体验。我们将逐步引导你完成配置,并提供代码示例

阅读时长: 5 分钟
共 2169字
作者: eimoon.com

还在为复杂的 CAPTCHA 验证困扰您的用户体验吗?Cloudflare Turnstile 为我们带来了一个优雅的解决方案。作为新一代的智能验证服务,它不仅能有效识别并拦截恶意机器人,更重要的是能让真实用户享受到丝滑般的网站体验。

为什么选择 Turnstile?

🚀 零感知验证 - 无需传统的图片识别验证码

🔒 强大的安全防护 - 智能识别并阻挡自动化攻击

🌐 通用性强 - 可在任何网站使用,不限于 Cloudflare 的站点

⚡ 性能出众 - 对网站性能影响微乎其微

这篇教程将带您一步步在 Next.js 项目中集成 Turnstile。虽然过程相对简单,但基于实践经验,我们会重点关注一些容易被忽视的技术细节,帮助您实现真正完美的集成。

一.创建 Cloudflare 账户并配置 Turnstile

首先,您需要:

创建或登录 Cloudflare 账户–访问 Turnstile 控制面板–点击"添加小组件"创建新的验证组件

alt text

在小部件设置中,添加您的域并选择“托管”模式,确保将其添加localhost到您的域以用于开发目的。

alt text

二. 获取必要的密钥

在创建验证组件后,您将获得两个重要的密钥:

Site Key: 用于客户端集成

Secret Key: 用于服务端验证

alt text

三. 项目结构设计

在开始编码之前,让我们先了解一下完整的项目结构

├── src/ 
│   ├── components/ 
│   │   ├── TurnstileWidget.tsx     # Turnstile 验证组件 
│   │   └── ContactForm.tsx         # 示例表单组件 
│   ├── app/ 
│   │   ├── api/ 
│   │   │   └── contact/ 
│   │   │       └── route.ts        # API 路由处理 
│   │   └── contact/ 
│   │       └── page.tsx            # 示例页面 
│   └── types/ 
│       └── turnstile.d.ts          # TypeScript 类型定义 
├── .env                             # 环境变量文件,存储 Turnstile 的密钥

四.初始化一个nextjs项目,并添加环境变量

在开始集成 Turnstile 之前,我们需要先创建并配置一个 Next.js 项目。

创建 Next.js 项目

# 使用 create-next-app 创建项目
npx create-next-app@latest my-turnstile-app
cd my-turnstile-app

添加env环境变量

在env文件中添加刚才获取的key

NEXT_PUBLIC_TURNSTILE_SITE_KEY=your_site_key_here
TURNSTILE_SECRET_KEY=your_secret_key_here

五.定义接口

首先让我们定义一个接口组件,这个接口将作为我们的基础验证组件。它主要提供以下功能:

  • 支持自定义验证栏的背景颜色(深色/浅色主题)
  • 提供多语言支持,方便国际化
  • 集成 Turnstile 的回调机制,处理验证结果
'use client';
import Script from 'next/script';
import { useCallback, useEffect, useRef } from 'react';

interface TurnstileWidgetProps {
  onVerify: (token: string) => void;
  //颜色
  theme?: 'light' | 'dark';
  //语言
  language?: string;
}

const TurnstileWidget: React.FC<TurnstileWidgetProps> = ({ 
  onVerify, 
  theme = 'light',
  language = 'zh-CN'
}) => {
  const divRef = useRef<HTMLDivElement>(null);
  
  const renderWidget = useCallback(() => {
        if (divRef.current && window.turnstile) {
          window.turnstile.render(divRef.current, {
            sitekey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY as string,
            callback: onVerify,
            theme,
            language,
          });
        }
      }, [theme, language, onVerify]);

  useEffect(() => {

   // 只在 turnstile 可用时渲染
    if (window.turnstile) {
      renderWidget();
    }
    const currentRef = divRef.current; 
    // 清理函数:组件卸载或依赖项改变时移除 widget
    return () => {
      if (currentRef) {
        window.turnstile?.remove(currentRef);
      }
    };
  },[renderWidget,theme, language, onVerify]);

  return(<>
        <Script
        src="https://challenges.cloudflare.com/turnstile/v0/api.js"
        onLoad= {
          renderWidget
        }
      />
      <div ref={divRef} />
  </> );
};

export default TurnstileWidget;

这个接口组件使用了 Next.js 的 Script 组件来按需加载 Turnstile 的脚本,这样可以优化页面加载性能。同时,我们还实现了完整的生命周期管理,确保组件在卸载时能够正确清理资源。

六.定义表单组件

接下来,我们创建一个实际的表单组件来展示如何使用 Turnstile 验证。这个组件的主要特点包括:

  • 集成了我们刚才创建的 Turnstile 接口组件
  • 实现了完整的表单状态管理
  • 提供了友好的加载和错误状态提示
  • 使用 TypeScript 确保类型安全
'use client';
import { useState, FormEvent } from 'react';
import TurnstileWidget from './TurnstileWidget';

interface FormData {
    email: string;
    message: string;
  }
  
  interface ApiResponse {
    message?: string;
    error?: string;
  }
  
const ContactForm: React.FC = () => {
  const [formData, setFormData] = useState<FormData>({
    email: '',
    message: '',
  });
  const [turnstileToken, setTurnstileToken] = useState<string>('');
  const [status, setStatus] = useState<string>('');
  const [isSubmitting, setIsSubmitting] = useState<boolean>(false);

  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    
    if (!turnstileToken) {
      setStatus('请完成验证码验证');
      return;
    }

    setIsSubmitting(true);

    try {
      const response = await fetch('/api/contact', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          ...formData,
          turnstileToken,
        }),
      });

      const data: ApiResponse = await response.json();
      
      if (response.ok) {
        setStatus('提交成功!');
        setFormData({ email: '', message: '' });
      } else {
        setStatus(`错误: ${data.error || '未知错误'}`);
      }
    } catch (error) {
      setStatus('提交失败,请重试');
      console.error('Form submission error:', error);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <div>
        <label htmlFor="email" className="block text-sm font-medium text-gray-700">
          邮箱:
        </label>
        <input
          type="email"
          id="email"
          value={formData.email}
          onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
          required
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
        />
      </div>
      
      <div>
        <label htmlFor="message" className="block text-sm font-medium text-gray-700">
          消息:
        </label>
        <textarea
          id="message"
          value={formData.message}
          onChange={(e) => setFormData(prev => ({ ...prev, message: e.target.value }))}
          required
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
          rows={4}
        />
      </div>

      <TurnstileWidget 
        onVerify={setTurnstileToken}
        theme="light"
      />
      
      {status && (
        <p className={`text-sm ${status.includes('错误') ? 'text-red-600' : 'text-green-600'}`}>
          {status}
        </p>
      )}
      
      <button 
        type="submit"
        disabled={isSubmitting}
        className={`px-4 py-2 rounded-md text-white ${
          isSubmitting 
            ? 'bg-blue-400 cursor-not-allowed' 
            : 'bg-blue-500 hover:bg-blue-600'
        }`}
      >
        {isSubmitting ? '提交中...' : '提交'}
      </button>
    </form>
  );
};

export default ContactForm;

组件中包含了一些关键的状态处理,表单数据状态管理,验证token的存储和处理,提交状态的追踪,错误信息的展示

七.定义 API 路由

为了处理表单提交,我们需要创建一个后端 API 路由。这个路由主要负责:

  • 接收前端提交的表单数据和验证token
  • 与 Cloudflare 服务器通信验证 token 的有效性
  • 处理验证结果并返回适当的响应
  • 然后定义一个API 路由类型和实现
interface TurnstileVerificationResponse {
    success: boolean;
    error_codes?: string[];
    challenge_ts?: string;
    hostname?: string;
}

interface RequestBody extends FormData {
    turnstileToken: string;
}

export async function POST(request: Request) {
    try {
        const body: RequestBody = await request.json();
        const { turnstileToken, ...formData } = body;

        // 验证 Turnstile token
        const verificationResponse = 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: turnstileToken,
                }),
            }
        );

        const verificationData: TurnstileVerificationResponse = await verificationResponse.json();

        if (!verificationData.success) {
            console.error('Turnstile verification failed:', verificationData['error_codes']);
            return Response.json(
                { error: '验证码验证失败' },
                { status: 400 }
            );
        }

        // 验证通过,处理表单数据
        // 这里添加你的业务逻辑,比如发送邮件或保存到数据库
        console.log(formData)

        return Response.json({ message: '提交成功' });
    } catch (error) {
        console.error('API route error:', error);
        return Response.json(
            { error: '服务器错误' },
            { status: 500 }
        );
    }
}

在这个部分,我们重点关注了几个安全相关的问题:

确保所有请求都经过 Turnstile 验证,妥善处理验证失败的情况,提供清晰的错误信息.

八.创建测试页面

最后,我们创建一个测试页面来集成所有组件。


import ContactForm from "../components/ContactForm";

export default function ContactPage() {
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">联系我们</h1>
      <ContactForm />
    </div>
  );
}

九.本地运行测试

alt text

结语 通过这个教程,我们不仅完成了 Turnstile 的基础集成,更重要的是掌握了一些实用的开发技巧。希望这些内容能帮助您在项目中构建更安全、更友好的用户验证机制。在实际部署时候,需要注意确保环境变量正确配置,检查域名设置是否包含了所有需要的域名. 如果您在集成过程中遇到任何问题,欢迎在评论区留言讨论

微信公众号

使用 Hugo 构建
主题 StackJimmy 设计