乐于分享
好东西不私藏

AI编程丨网站如何添加人机验证?

AI编程丨网站如何添加人机验证?

你好,我是一卒。

因为我们的网站里面有调用 API,而调用 API 我们是要花钱的。所以为了避免被别人用机器人注册账号来薅羊毛,我们需要给我们的网站增加一个防护措施。

文章里面涉及到一些代码,在电脑上阅读体验更好。

这个防护措施就是给网站增加一个功能,来检查访问的是真人还是机器人脚本。

这个功能就是人机验证。人机验证这种方式有多种,这里我们选择 Cloudflare Turnstile。

Cloudflare 是一家美国互联网基础设施与网络安全公司,我们上篇文章提到的 DNS 域名解析就是这家公司的强项。Cloudflare 提供免费的 DNS 服务,而且安全性很高

Turnstile 直译过来是“旋转闸门”的意思,Cloudflare Turnstile 就像在这个闸门口放了一个保安,这个保安会验证来访问的是真人还是机器人

Cloudflare Turnstile 人机验证的效果是这样的:

一、选择 Cloudflare Turnstile 的理由

  • 完全免费,没有请求量限制
  • 用户体验好:不像谷歌的 reCAPTCHA 人机验证,要点红绿灯,自行车等,很麻烦
  • 接入简单:前端加个组件,后端加个验证接口

Cloudflare Turnstile 有三种模式,分别是Managed托管模式、Non-Interactive非交互模式、Invisible 隐身模式

直接选择托管模式即可

托管模式的意思是,Cloudflare 会根据访问者的风险等级来决定如何验证:

  1. 如果 Cloudflare 觉得是真人,就直接放行,用户什么都看不到。
  2. 如果 Cloudflare 觉得有可能是真人,也可能不是真人,那就会在页面上出现一个小方块,让用户点击一下。
  1. 如果 Cloudflare 觉得是机器人,则会直接验证失败。

二、如何配置 Cloudflare Turnstile 人机验证?

网址:https://www.cloudflare.com/

  1. 点击 Turnstile
  1. 点击 Add widget 添加组件,添加域名
  1. 选择Managed托管模式,点击创建
  1. 复制站点密钥和私密密钥

这两个密码不要泄露,要在后端添加到环境变量里使用。

Site Key 泄露:几乎没有风险

Secret Key 泄露:风险很大

它的作用是让你的后端服务器去 Cloudflare 验证"用户提交的 token 是不是真的"。如果它泄露了,

人机验证直接失效。

任何人拿到你的 Secret Key,就可以自己写代码,伪造一个"验证通过"的响应发给你的后端。你的后端以为验证通过了,实际上根本没有经过 Turnstile 检查。机器人就可以畅通无阻地注册账号、薅你的 AI 额度,就像没加人机验证一样。之前做的所有防护都白费了。

  1. 把两个key保存在环境变量里

在项目根目录下面的 .env.local 文件添加这密钥:

# Cloudflare Turnstile 配置

NEXT_PUBLIC_TURNSTILE_SITE_KEY=0x4AAAAAADOpKhlVxxxxxxxxx

TURNSTILE_SECRET_KEY=0x4AAAAAADOpKjQzrS4UQG9Jxxxxxxxxxxx

SITE_KEY要加"NEXT_PUBLIC"前缀

不加 NEXT_PUBLIC_ 前缀,前端代码就读不到这个变量,Turnstile 组件会拿不到 Site Key,验证功能直接无法工作。

这是 Next.js 的一个安全设计机制。Next.js 把环境变量分成两类:

不带 NEXT_PUBLIC_ 前缀的变量,只存在于服务器端(后端)。当你的前端页面在用户浏览器里运行时,这些变量根本不会被打包进去,前端代码访问它们只会得到 undefined(空值)。

带 NEXT_PUBLIC_ 前缀的变量,Next.js 会在构建时把它的值直接放进前端代码里,这样浏览器端也能访问到。

调试的时候也可以用这个官方测试的 key 先跑通流程

官方测试 Key:  - sitekey: 1x00000000000000000000AA  - secret: 1x0000000000000000000000000000000AA
  1. 安装前端组件

在终端运行

npm install @marsidev/react-turnstile

这是一个非常简单的第三方依赖包。

@marsidev/react-turnstile 本质上就是有人把 Cloudflare 官方的 Turnstile 脚本封装成了一个 React 组件,让你不用自己手动写那些底层的 JS 初始化代码。

运行 npm install @marsidev/react-turnstile 之后,你只需要做两件事:在文件顶部 import 它,然后把组件放到表单里,传入 Site Key 和一个回调函数就完了。代码量非常少。

  1. 在注册页面添加人机验证组件

我们要做的,是在注册表单里加一个"验证组件"。当用户填完信息准备注册时,这个组件会悄悄在后台判断他是不是真人,判断通过后会给你一张"通行证"(token),你再把这张通行证连同注册信息一起发给后端,后端拿着这张通行证去 Cloudflare 核验,核验通过才允许注册。

整个前端改动分三步。

第一步:导入组件库(Import)

import { Turnstile } from '@marsidev/react-turnstile'

意思是:告诉你的代码文件"我要用一个叫 Turnstile 的组件,它来自 @marsidev/react-turnstile 这个安装好的包"。不写这行,后面用 <Turnstile /> 时代码会报错,因为它不知道这个东西是什么。

这行写在文件最顶部,和其他 import 语句放在一起。

第二步:声明状态变量(State Declaration)

const [turnstileToken, setTurnstileToken] = useState('');

在 React 里,页面上任何会变化的数据,都需要用 useState 来存储。这里我们需要存的是 Turnstile 验证通过后返回的 token(通行证)。

为什么要这一步? 因为第三步的组件里用到了 setTurnstileToken,如果没有提前声明,代码会报错"找不到这个函数"。

第三步:放置验证组件(Component Placement)

<Turnstile  siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}  onSuccess={(token) => {    setTurnstileToken(token)  }}/>

这段代码放在表单里、注册按钮的上方。它做了两件事:

  • siteKey:告诉 Turnstile 组件你的站点密钥是什么,它用这个去联系 Cloudflare 做验证。这里用的是环境变量,而不是直接写死密钥,是为了安全。
  • onSuccess 就像你在便利店门口装了一个人脸识别闸机。

闸机平时是关着的。当它扫描完你的脸、确认你是真人之后,会做两件事:第一,把门打开让你进去;第二,打印一张小票给你,上面写着"此人已验证,是真人"。

(token) => {  setTurnstileToken(token)}

这个代码就是在说:"等闸机验证成功、把小票打出来之后,我把这张小票收好(存进 turnstileToken 这个变量里)。"

验证没通过之前,这段代码什么都不做,小票也不会有。只有验证成功的那一刻,它才会触发,把 token 这张"通行证小票"保存起来,等用户点注册按钮的时候,再把它交给后端查验。

为什么放在注册按钮上方? 纯粹是用户体验的考虑——用户填完信息,看到验证组件,验证通过后再点注册按钮,流程自然顺畅。

什么叫回调函数?

"回调"的意思就是:等某件事发生了,再来执行这段代码。

用生活比喻:你叫了外卖,然后告诉自己"等外卖到了,我就去开门"。这个"等外卖到了再执行的动作",就是回调。

onSuccess={(token) => {  setTurnstileToken(token)}}

意思是:"等 Turnstile 验证成功了,再执行 setTurnstileToken(token) 这行代码"。验证没成功之前,这段代码不会运行。

  1. 后端添加验证逻辑

前端是可以绕过的,所以必须要在后端的注册页面的逻辑下面加上人机验证,才算真正完成了整个步骤。

在后端注册逻辑的代码文件里,加上这段代码

// 从请求体中拿到前端传来的 Turnstile tokenconst { turnstileToken, ...registrationData } = await request.json()// 去 Cloudflare 验证这个 token 是不是真的const verifyResponse = awaitfetch('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 verifyResult = await verifyResponse.json()// 如果验证失败,直接拒绝if (!verifyResult.success) {returnResponse.json(    { error:'人机验证失败,请重试' },    { status:403 }  )}// 验证通过,继续正常的注册流程...

如果不会改,就把这段代码丢给AI,让AI来帮你改,这是AI改完之后的代码:

import { NextRequestNextResponse } from'next/server';import bcrypt from'bcryptjs';import { getSupabaseClient } from'@/storage/database/supabase-client';exportasyncfunctionPOST(request: NextRequest) {try {const { username, password, turnstileToken } = await request.json();  // 👈 这里加了 turnstileToken// Turnstile 人机验证  👈 这一整块是新加的const verifyResponse = awaitfetch('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 verifyResult = await verifyResponse.json();if (!verifyResult.success) {returnNextResponse.json({ error:'人机验证失败,请重试' }, { status:403 });    }// 人机验证结束 👆if (!username || !password) {returnNextResponse.json({ error:'用户名和密码不能为空' }, { status:400 });    }if (username.length < 2 || username.length > 50) {returnNextResponse.json({ error:'用户名长度需在2-50之间' }, { status:400 });    }if (password.length < 6) {returnNextResponse.json({ error:'密码长度不能少于6位' }, { status:400 });    }const supabase = getSupabaseClient();const { data: existing } = await supabase      .from('users')      .select('id')      .eq('username', username)      .maybeSingle();if (existing) {returnNextResponse.json({ error:'用户名已存在' }, { status:409 });    }const hashedPassword = await bcrypt.hash(password, 10);const { data, error } = await supabase      .from('users')      .insert({ username, password: hashedPassword })      .select('id, username, created_at')      .single();if (error) {console.error('Register DB error:', error);returnNextResponse.json({ error:'注册失败,请重试' }, { status:500 });    }returnNextResponse.json({user: { id: data.idusername: data.username },    });  } catch (err) {console.error('Register API error:', err);returnNextResponse.json({ error:'注册失败,请重试' }, { status:500 });  }}

总结一下改了哪两个地方:

第一处,第一行读取请求数据的地方,原来只读 username 和 password,现在多读一个 turnstileToken:

// 改前const { username, password } = await request.json();// 改后const { username, password, turnstileToken } = await request.json();

第二处,紧接着在这行下面,插入整块 Turnstile 验证逻辑。它必须放在最前面,这样机器人的请求在进入任何注册流程之前就被拦截掉了。

注意事项:

第一点:验证地址是固定的

https://challenges.cloudflare.com/turnstile/v0/siteverify 这个地址是 Cloudflare 官方提供的"核验窗口",全世界所有用 Turnstile 的人都用同一个地址。你不需要理解它,照抄就行,一个字母都不要改。

第二点:每张通行证只能用一次

token 就像电影票,撕过一次就作废了。用户注册成功后,这张 token 就失效了,下次注册必须重新验证、拿一张新的。这是 Cloudflare 故意设计的,防止有人拿一张通行证反复用。

第三点:通行证有5分钟有效期

用户打开注册页面,Turnstile 验证通过,发了一张 token 给你。但如果用户发呆了10分钟才点注册按钮,这张 token 已经过期了,后端验证会失败。遇到这种情况,让用户刷新页面重新验证一次就好了,这是正常现象。

设置好之后,先让 AI 验证一下看是否有 Bug 需要修复

  1. 最后验证

配置好人机验证之后,正常的用户会看到注册页面出现一个小方块的人机验证按钮。

如果是机器人批量注册,通过脚本调用注册接口,由于它拿不到 Cloudflare Turnstile token,后端验证就会失败,注册会被拒绝。即使它伪造了 token,Cloudflare 也会验证失败,注册同样会被拒绝。

这样我们的 API 就不会被薅羊毛了。只要网站有注册或免费的资源,比如调用API 就一定要加人机验证。

测试成功:

感谢你看到这里,下期分享怎么给网站添加云存储的功能。

[AI编程] 系列文章:

1.AI编程丨歧义是最大的bug

2.AI编程丨消除歧义的方法:SPEC

3.AI编程丨程序运行前提:安装环境及依赖

4.AI编程丨如何正确调用API?

5.AI编程丨数据库必备知识

6.AI编程丨新手怎么设计前端界面?

7.AI编程丨如何精准处理代码报错?

8.AI编程丨如何把网站部署上线?

9.AI编程丨如何给网站绑定专属域名?