01MVP 标识01MVP
包文档安全Captcha

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

字段类型说明
successboolean是否通过
errorCodesstring[]?错误码
challengeTsstring?时间戳
hostnamestring?域名

实战示例

示例 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 密钥

  1. 登录 Cloudflare Dashboard
  2. 进入 Turnstile 页面
  3. 添加站点,获取 Site Key 和 Secret Key
  4. 设置环境变量

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 />

最佳实践

  1. 仅在生产环境启用 - 开发时禁用避免频繁验证
  2. 服务端验证 - 永远在服务端验证 token,不要信任客户端
  3. 传递 IP - 通过 x-forwarded-for 传入 IP 提高准确性
  4. 错误处理 - 验证失败时给用户重试机会
  5. 主题适配 - Turnstile 支持 theme: 'auto' 自动适配暗色模式

故障排查

验证始终失败

  1. 检查 TURNSTILE_SECRET_KEY 是否正确
  2. 确认域名已在 Cloudflare Turnstile 中配置
  3. 开发环境使用测试密钥:1x0000000000000000000000000000000AA

Widget 不显示

  1. 确认已加载 Turnstile 脚本
  2. 检查 NEXT_PUBLIC_TURNSTILE_SITE_KEY 是否设置
  3. 确认容器 DOM 元素已渲染

相关文档

许可证

MIT