还在为复杂的 CAPTCHA 验证困扰您的用户体验吗?Cloudflare Turnstile 为我们带来了一个优雅的解决方案。作为新一代的智能验证服务,它不仅能有效识别并拦截恶意机器人,更重要的是能让真实用户享受到丝滑般的网站体验。
为什么选择 Turnstile? 🚀 零感知验证 – 无需传统的图片识别验证码
🔒 强大的安全防护 – 智能识别并阻挡自动化攻击
🌐 通用性强 – 可在任何网站使用,不限于 Cloudflare 的站点
⚡ 性能出众 – 对网站性能影响微乎其微
这篇教程将带您一步步在 Next.js 项目中集成 Turnstile。虽然过程相对简单,但基于实践经验,我们会重点关注一些容易被忽视的技术细节,帮助您实现真正完美的集成。
这里的markdown排版不太好用,可以我的博客阅读体验会好一些
一.创建 Cloudflare 账户并配置 Turnstile 首先,您需要:
创建或登录 Cloudflare 账户–访问 Turnstile 控制面板–点击”添加小组件”创建新的验证组件
创建组件
在小部件设置中,添加您的域并选择“托管”模式,确保将其添加localhost到您的域以用于开发目的。
设置模式
二. 获取必要的密钥 在创建验证组件后,您将获得两个重要的密钥:
Site Key: 用于客户端集成
Secret Key: 用于服务端验证
获取密钥
三. 项目结构设计 在开始编码之前,让我们先了解一下完整的项目结构
带插入图片
四.初始化一个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 = 'dark', 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; password: string; } interface ApiResponse { message?: string; error?: string; } // 添加状态类型 type StatusType = 'success' | 'error' | ''; const ContactForm = () => { const [formData, setFormData] = useState<FormData>({ email: '', password: '', }); const [turnstileToken, setTurnstileToken] = useState<string>(''); const [status, setStatus] = useState<{ type: StatusType; message: string }>({ type: '', message: '' }); const [isSubmitting, setIsSubmitting] = useState<boolean>(false); const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { e.preventDefault(); if (!turnstileToken) { setStatus({ type: 'error', message: '请完成人机验证' }); 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({ type: 'success', message: '提交成功!' }); setFormData({ email: '', password: '' }); } else { setStatus({ type: 'error', message: `错误: ${data.error || '未知错误'}` }); } } catch (error) { setStatus({ type: 'error', message: '提交失败,请重试' }); console.error('Form submission error:', error); } finally { setIsSubmitting(false); } }; return ( <form onSubmit={handleSubmit} className="mx-auto mt-16 max-w-xl sm:mt-20"> <div className="grid grid-cols-1 gap-x-8 gap-y-6 sm:grid-cols-2"> <div className="sm:col-span-2"> <label htmlFor="email" className="block text-sm/6 font-semibold text-gray-900"> 邮箱 </label> <div className="mt-2.5"> <input type="email" id="email" value={formData.email} onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))} required className="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm/6" /> </div> </div> <div className="sm:col-span-2"> <label htmlFor="password" className="block text-sm/6 font-semibold text-gray-900"> 密码 </label> <div className="mt-2.5"> <input type="password" id="password" value={formData.password} onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))} required className="block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm/6" /> </div> </div> <div className="sm:col-span-2"> <TurnstileWidget onVerify={setTurnstileToken} theme="dark" language='en' /> </div> {status.message && ( <div className="sm:col-span-2"> <p className={`text-sm ${status.type === 'error' ? 'text-red-600' : 'text-green-600'}`}> {status.message} </p> </div> )} </div> <div className="mt-10"> <button type="submit" disabled={isSubmitting} className={`block w-full rounded-md px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm ${ isSubmitting ? 'bg-indigo-400 cursor-not-allowed' : 'bg-indigo-600 hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600' }`} > {isSubmitting ? '提交中...' : '提交'} </button> </div> </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> ); }
九.本地运行测试
结语
原文通过这个教程,我们不仅完成了 Turnstile 的基础集成,更重要的是掌握了一些实用的开发技巧。希望这些内容能帮助您在项目中构建更安全、更友好的用户验证机制。在实际部署时候,需要注意确保环境变量正确配置,检查域名设置是否包含了所有需要的域名. 如果您在集成过程中遇到任何问题,欢迎在评论区留言讨论。