You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js/React集成Cloudflare Turnstile表单无法获取响应令牌求助

问题:Next.js 13 集成Cloudflare Turnstile后无法获取验证令牌

我是Next.js/React新手,正在构建简易联系表单并使用Cloudflare Turnstile防护。未集成Turnstile时,表单可正常提交到API并发送邮件,但集成后无法获取响应令牌——检查元素能看到输入框已填充令牌,但提交时token始终返回null。

环境

  • Next.js 13(使用'use client'指令)
  • 依赖:@marsidev/react-turnstile

表单代码

<form onSubmit={handleSubmit}>
  <div className="flex flex-col gap-4">
    <div className="form-control w-full max-w-md">
      <label className="label" htmlFor="name">
        <span className="label-text">Name</span>
      </label>
      <input
        type="text"
        id="name"
        placeholder="Your Name"
        required
        className="input w-full max-w-md"
        name="name"
        onChange={(e) => setName(e.target.value)}
      />
    </div>
    <div className="form-control w-full max-w-md">
      <label className="label" htmlFor="email">
        <span className="label-text">Email</span>
      </label>
      <input
        type="text"
        id="email"
        placeholder="Your Email"
        required
        className="input w-full max-w-md"
        name="email"
        onChange={(e) => setEmail(e.target.value)}
      />
    </div>
    <div className="form-control w-full max-w-md">
      <label className="label" htmlFor="subject">
        <span className="label-text">Subject</span>
      </label>
      <input
        type="text"
        id="subject"
        placeholder="Subject"
        required
        className="input w-full max-w-md"
        name="subject"
        onChange={(e) => setSubject(e.target.value)}
      />
    </div>
    <div className="form-control">
      <label className="label" htmlFor="message">
        <span className="label-text">Message</span>
      </label>
      <textarea
        className="textarea-bordered textarea h-48 w-full max-w-md"
        id="message"
        placeholder="Your message"
        required
        name="message"
        onChange={(e) => setMessage(e.target.value)}
      ></textarea>
    </div>
    <Turnstile siteKey={cfSiteKey} />
    <button type="submit" className="btn-primary btn w-full max-w-md">
      Submit
    </button>
  </div>
</form>

处理函数代码

let cfSiteKey: string;

const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [subject, setSubject] = useState('');
const [message, setMessage] = useState('');
const [submitted, setSubmitted] = useState(false);

if (process.env.NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY) {
  cfSiteKey= process.env.NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY;
} else {
  throw new Error('Cloudflare Turnstile Secret Key Not Set');
}

const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault();
  const formData = new FormData();
  const token = formData.get('cf-turnstile-response');
  console.log(token); // 始终返回null

  const data = {
    name,
    email,
    subject,
    message,
    token,
  };
  fetch('/api/contact', {
    method: 'POST',
    headers: {
      Accept: 'application/json, text/plain, */*',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
  }).then((res) => {
    console.log('Response received');
    if (res.status === 200) {
      console.log('Response succeeded!');
      setSubmitted(true);
      setName('');
      setEmail('');
      setSubject('');
      setMessage('');
    }
  });
};

解决方法

核心问题分析

你创建FormData时没有传入表单元素,导致生成的FormData是空对象,自然无法获取到cf-turnstile-response字段的值。另外,更可靠的方式是直接通过Turnstile组件的回调来获取令牌,避免依赖FormData的自动收集。

方法一:修复FormData的创建逻辑

在handleSubmit中,将表单元素传入FormData构造函数,这样它会自动收集所有表单字段:

const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault();
  // 传入表单元素到FormData,自动收集所有字段
  const formData = new FormData(e.currentTarget as HTMLFormElement);
  const token = formData.get('cf-turnstile-response');
  console.log(token); // 现在能正确获取令牌

  // 后续逻辑不变...
};

方法二:通过Turnstile组件的onChange回调保存令牌(更推荐)

这种方式直接从组件获取令牌,无需依赖FormData,逻辑更清晰可靠:

  1. 新增状态存储令牌:
const [token, setToken] = useState('');
  1. 修改Turnstile组件,添加onChange回调:
<Turnstile siteKey={cfSiteKey} onChange={setToken} />
  1. 在handleSubmit中直接使用token状态:
const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault();

  // 先校验令牌是否存在
  if (!token) {
    console.error('请完成Turnstile验证');
    return;
  }

  const data = {
    name,
    email,
    subject,
    message,
    token,
  };

  fetch('/api/contact', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
  })
  .then((res) => {
    if (res.ok) {
      setSubmitted(true);
      setName('');
      setEmail('');
      setSubject('');
      setMessage('');
      setToken(''); // 重置令牌状态
    } else {
      console.error('提交失败');
    }
  })
  .catch(err => console.error('提交出错:', err));
};

额外优化点

  • 简化cfSiteKey的获取方式,直接使用断言(因为是public环境变量,构建时必须存在):
const cfSiteKey = process.env.NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY!;
  • 给fetch添加catch处理,避免未捕获的错误导致页面无响应。

内容的提问来源于stack exchange,提问作者BWBama85

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 12:00:58