---
type: tutorial
title: 把组件添加到你的站点
summary: 把组件添加到你的 React 联系表单，在仪表盘里确认一次真实的验证，然后在你的 Node 后端上强制执行它。
---

你已经有了 [上一节课](/docs/getting-started/create-your-account) 里 BananaSeed 的公钥。现在我们把组件放到 React 联系表单上，看着仪表盘确认一位真实访客通过了它，然后加上那一个把它变成真正防护的服务器调用。

## 你在结束时会拥有什么

- 组件已在你的联系表单上，在浏览器里验证访客。
- 你的仪表盘显示出那些验证。
- 你的 Node 后端丢弃任何没有有效令牌的提交。

## 1. 把组件添加到你的 React 表单

安装这个包。它会注册 `<caputchin-widget>` 元素（以及给下一节课用的 `<caputchin-game>` 元素）：

```bash
npm install @caputchin/widget
```

在启动时导入它一次，比如放在你的应用入口（`main.jsx`）里：

```js
import "@caputchin/widget";
```

现在把组件放进 BananaSeed 的联系表单。当复选框通过时，组件会往表单里加一个隐藏的 `caputchin-token` 字段，于是你在提交时读它就和读你自己的字段一模一样。把 `cpt_pub_...` 换成你的公钥：

```jsx
export function ContactForm() {
  async function handleSubmit(e) {
    e.preventDefault();
    const data = new FormData(e.currentTarget);
    await fetch("/api/contact", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        email: data.get("email"),
        message: data.get("message"),
        token: data.get("caputchin-token"),
      }),
    });
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <caputchin-widget sitekey="cpt_pub_..." />
      <button type="submit">Send</button>
    </form>
  );
}
```

在一个没有构建步骤的纯 HTML 页面上，或者在一个原生移动应用里？见 [script 标签（CDN）集成](/docs/integration-guides/cdn) 和 [移动应用指南](/docs/integration-guides/mobile-apps)。

## 2. 看它验证

运行你的应用并打开联系表单。Caputchin 复选框会出现并自行完成，无需点击。你甚至还不用提交表单：那已经是一次真实的验证，而且它已经到达了 Caputchin。

## 3. 在你的仪表盘里确认

回到仪表盘，打开你的团队，点进你的站点密钥。它的统计显示一条漏斗：**会话已开始**、**客户端已完成**、**服务端已核验**、以及 **已拦截威胁**。刷新页面；**会话已开始** 和 **客户端已完成** 现在都至少读作 `1`。那就是你的这次访问：组件加载了（已开始），挑战在浏览器里被解开了（客户端已完成），完全没有后端参与。

**服务端已核验** 仍是 `0`，因为你服务器上还没有任何东西核验过令牌。那个缺口就是下一步。

<Callout type="success">你的组件已经上线，仪表盘也已经在记录真实的验证，而你还没写一行后端代码。🎉</Callout>

## 4. 在你的后端核验令牌

客户端已完成意味着访客通过了挑战，但在你的服务器确认令牌之前，你的表单并未受保护。机器人可以完全无视组件，直接向你的端点 POST。所以你的后端会先用 Caputchin 核验每一个令牌，然后才信任该请求。你的密钥（`cpt_sec_...`）存在一个环境变量里，绝不放进浏览器：

```js
import express from "express";

const app = express();
app.use(express.json());

app.post("/api/contact", async (req, res) => {
  const { email, message, token } = req.body;

  const verdict = await fetch("https://verify.caputchin.com/v1/siteverify", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      secret: process.env.CAPUTCHIN_SECRET,
      response: token,
    }),
  }).then((r) => r.json());

  if (!verdict.success) {
    return res.status(400).json({ error: "Could not verify you are human." });
  }

  // Verified. Handle the real message: store it, email it, whatever you do.
  res.json({ ok: true });
});

app.listen(3001);
```

那个 `verdict.success` 检查正是要害所在：一个直接向 `/api/contact` 发帖的机器人不带任何令牌，`success` 返回 `false`，于是消息在你碰到它之前就被丢弃了。

没有后端？没问题。打开 [托管验证](/docs/hosted-verification/overview)，Caputchin 就替你跑这道检查，只把已核验的提交转发到一个 webhook 或你的收件箱。

## 5. 确认服务端检查

既然你的端点正在验证，就再提交一次联系表单。回到你站点密钥的统计页并刷新：**服务端已核验** 现在也至少读作 `1` 了，与已开始和客户端已完成并排。整条漏斗都对上了账，从浏览器一直到服务器。

要反过来读这条漏斗（各计数之间的缺口在告诉你访客流失和集成失误的什么信息），见 [统计与会话](/docs/site-keys/statistics)。

<Callout type="success">你的联系表单现在是真正受保护的了：每一次提交都在你的服务器上被核验，无令牌的机器人被拒之门外。🎉</Callout>

## 刚刚发生了什么

| 步骤 | 在哪里 | 结果 |
|---|---|---|
| 你表单里的 `<caputchin-widget>` | React | 复选框在挂载时验证，并加上一个一次性的 `caputchin-token` 字段。 |
| 打开页面 | 浏览器 | 已开始和客户端已完成跳动，无需后端。 |
| 带上你密钥的 `/siteverify` | Node 到 Caputchin | `success: true` 放请求通过；没有令牌就把它丢弃，服务端已核验随之攀升。 |

## 常见问题

### 用这个组件需要后端吗？

对于浏览器这一步，不需要。对于真正的保护，需要。组件会在浏览器里自己验证，但机器人可以无视它、直接向你的端点 POST，所以在信任一次提交之前，你的服务器必须先确认 `caputchin-token`。如果你不跑后端，就打开 [托管验证](/docs/hosted-verification/overview)，Caputchin 就替你跑这道检查。

### 为什么组件加载之后，服务端已核验还是零？

因为你的服务器还没有核验过任何令牌。已开始和客户端已完成只靠浏览器就跳动；服务端已核验只有在你的后端调用 `/siteverify` 之后才会攀升，那就是 [第 4 步](#4-在你的后端核验令牌)。

### 组件在 React 之外能用吗？

能。它是一个标准的自定义元素，所以用同样的一个标签就能落进纯 HTML、Vue、Svelte、Angular，或者原生应用的 WebView。见 [客户端集成](/docs/integration-guides/cdn) 和 [移动应用指南](/docs/integration-guides/mobile-apps)。

### 我的密钥放哪里？

只在你的服务器上，放进一个环境变量（`cpt_sec_...`）。它为 `/siteverify` 调用做身份验证，绝不能发到浏览器。

## 下一步

复选框能用了。现在到了有趣的部分：把它换成一个真正可玩的游戏，而你的后端一点都不用改。

继续前往 [从应用市场添加一个游戏](/docs/getting-started/add-a-game)。
