Captcha
人机验证服务,支持 Cloudflare Turnstile
@mono/captcha
人机验证(CAPTCHA)服务包,支持 Cloudflare Turnstile,保护表单和 API 免受机器人攻击。提供零依赖、类型安全的验证接口。
特性
- Cloudflare Turnstile - 无感验证,用户体验友好
- 可禁用 - 开发环境可关闭,自动通过
- 类型安全 - 完整 TypeScript 类型
- 零依赖 - 仅使用 fetch API
安装
pnpm add @mono/captcha快速开始
import { createCaptchaVerifier } from '@mono/captcha';
const captcha = createCaptchaVerifier({
enabled: true,
provider: 'cloudflare-turnstile',
cloudflare: {
secretKey: process.env.TURNSTILE_SECRET_KEY!,
siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!,
},
});
// 验证 token
const result = await captcha.verify(token);
if (!result.success) {
throw new Error('Captcha failed');
}API 参考
createCaptchaVerifier(config)
创建验证器。enabled: false 时自动通过所有验证。
参数:
config.enabled: boolean - 是否启用config.provider:'cloudflare-turnstile'config.cloudflare.secretKey: string - 服务端密钥config.cloudflare.siteKey: string - 客户端密钥
返回:
verify(token, remoteIp?)- 验证函数isEnabled- 是否启用
createCaptchaProvider(type, config)
直接创建提供商实例。
CaptchaVerifyResult
| 字段 | 类型 | 说明 |
|---|---|---|
success | boolean | 是否通过 |
errorCodes | string[]? | 错误码 |
challengeTs | string? | 时间戳 |
hostname | string? | 域名 |
实战示例
示例 1: API 路由保护
// app/api/auth/signup/route.ts
import { captcha } from '@/lib/captcha';
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
const { email, password, captchaToken } = await req.json();
// 验证人机
const verification = await captcha.verify(
captchaToken,
req.headers.get('x-forwarded-for') || undefined,
);
if (!verification.success) {
return NextResponse.json(
{ error: '人机验证失败,请重试' },
{ status: 403 },
);
}
// 继续注册逻辑...
return NextResponse.json({ success: true });
}示例 2: 客户端 Turnstile 组件
'use client';
import { useEffect, useRef, useCallback } from 'react';
import Script from 'next/script';
interface TurnstileProps {
siteKey: string;
onVerify: (token: string) => void;
onError?: () => void;
}
export function Turnstile({ siteKey, onVerify, onError }: TurnstileProps) {
const containerRef = useRef<HTMLDivElement>(null);
const widgetId = useRef<string | null>(null);
const renderWidget = useCallback(() => {
if (!containerRef.current || widgetId.current) return;
// @ts-ignore - Turnstile global
widgetId.current = window.turnstile?.render(containerRef.current, {
sitekey: siteKey,
callback: onVerify,
'error-callback': onError,
theme: 'auto',
});
}, [siteKey, onVerify, onError]);
useEffect(() => {
renderWidget();
return () => {
if (widgetId.current) {
// @ts-ignore
window.turnstile?.remove(widgetId.current);
widgetId.current = null;
}
};
}, [renderWidget]);
return (
<>
<Script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
onLoad={renderWidget}
/>
<div ref={containerRef} />
</>
);
}示例 3: 注册表单集成
'use client';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { Button, Input, Form } from '@mono/ui';
import { Turnstile } from '@/components/Turnstile';
export function SignupForm() {
const [captchaToken, setCaptchaToken] = useState('');
const form = useForm({ defaultValues: { email: '', password: '' } });
const onSubmit = async (data: any) => {
if (!captchaToken) {
alert('请完成人机验证');
return;
}
const res = await fetch('/api/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...data, captchaToken }),
});
if (res.ok) {
window.location.href = '/';
}
};
return (
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<Input {...form.register('email')} type="email" placeholder="邮箱" />
<Input {...form.register('password')} type="password" placeholder="密码" />
<Turnstile
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
onVerify={setCaptchaToken}
/>
<Button type="submit" className="w-full" disabled={!captchaToken}>
注册
</Button>
</form>
);
}示例 4: 配置初始化
// lib/captcha.ts
import { createCaptchaVerifier } from '@mono/captcha';
export const captcha = createCaptchaVerifier({
enabled: process.env.NODE_ENV === 'production',
provider: 'cloudflare-turnstile',
cloudflare: {
secretKey: process.env.TURNSTILE_SECRET_KEY || '',
siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || '',
},
});示例 5: 与 better-auth 集成
// lib/auth/config.ts
import { captcha } from '@/lib/captcha';
export const authConfig = {
// 在认证钩子中验证 captcha
hooks: {
before: async (context: any) => {
if (context.path === '/sign-up' || context.path === '/sign-in') {
const token = context.body?.captchaToken;
if (captcha.isEnabled && token) {
const result = await captcha.verify(token);
if (!result.success) {
throw new Error('Captcha verification failed');
}
}
}
},
},
};示例 6: 联系表单保护
// app/api/contact/route.ts
import { captcha } from '@/lib/captcha';
import { emailProvider } from '@/lib/email';
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
const { name, email, message, captchaToken } = await req.json();
// 人机验证
const verification = await captcha.verify(captchaToken);
if (!verification.success) {
return NextResponse.json({ error: '验证失败' }, { status: 403 });
}
// 发送邮件
await emailProvider.send({
to: 'admin@yourdomain.com',
subject: `联系表单: ${name}`,
html: `<p>${message}</p>`,
replyTo: email,
});
return NextResponse.json({ success: true });
}环境变量
# Cloudflare Turnstile 密钥
TURNSTILE_SECRET_KEY=0x4AAAAAAA...
NEXT_PUBLIC_TURNSTILE_SITE_KEY=0x4AAAAAAA...获取密钥:Cloudflare Turnstile Dashboard
集成指南
1. 获取 Turnstile 密钥
- 登录 Cloudflare Dashboard
- 进入 Turnstile 页面
- 添加站点,获取 Site Key 和 Secret Key
- 设置环境变量
2. 初始化服务
// lib/captcha.ts
import { createCaptchaVerifier } from '@mono/captcha';
export const captcha = createCaptchaVerifier({
enabled: process.env.NODE_ENV === 'production',
provider: 'cloudflare-turnstile',
cloudflare: {
secretKey: process.env.TURNSTILE_SECRET_KEY || '',
siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || '',
},
});3. 前端引入 Turnstile
在 layout.tsx 或需要的页面引入 Turnstile 脚本:
<Script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer />最佳实践
- 仅在生产环境启用 - 开发时禁用避免频繁验证
- 服务端验证 - 永远在服务端验证 token,不要信任客户端
- 传递 IP - 通过
x-forwarded-for传入 IP 提高准确性 - 错误处理 - 验证失败时给用户重试机会
- 主题适配 - Turnstile 支持
theme: 'auto'自动适配暗色模式
故障排查
验证始终失败
- 检查
TURNSTILE_SECRET_KEY是否正确 - 确认域名已在 Cloudflare Turnstile 中配置
- 开发环境使用测试密钥:
1x0000000000000000000000000000000AA
Widget 不显示
- 确认已加载 Turnstile 脚本
- 检查
NEXT_PUBLIC_TURNSTILE_SITE_KEY是否设置 - 确认容器 DOM 元素已渲染
相关文档
- Cloudflare Turnstile 文档
- @mono/auth 包 - 认证系统
- @mono/email 包 - 邮件服务
许可证
MIT