在 Nextjs 中无缝集成 Cloudflare Turnstile
likun
Posted on November 1, 2024
还在为复杂的 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 的基础集成,更重要的是掌握了一些实用的开发技巧。希望这些内容能帮助您在项目中构建更安全、更友好的用户验证机制。在实际部署时候,需要注意确保环境变量正确配置,检查域名设置是否包含了所有需要的域名. 如果您在集成过程中遇到任何问题,欢迎在评论区留言讨论。
Posted on November 1, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.