# 其他项目接入文档

本系统可以作为独立验证码服务，被其他业务项目接入使用。

适用场景：登录、注册、找回密码、短信发送、支付等关键操作前的人机校验。

## 一、准备工作

1. 在管理后台创建应用：`/admin/dashboard.php` -> 创建应用。
2. 拿到以下参数：
   - `app_id`
   - `app_key`
   - `app_secret`（仅后端保存，不要暴露到前端）
3. 可选：为该应用上传背景图（支持多图随机）。

## 二、接入方式总览

- 前端：使用 `public/assets/slider-captcha.js` 渲染滑块。
- 后端：调用本系统 API 完成 challenge 获取、verify 校验、ticket 消费。

核心流程：

1. 前端请求 `/api/captcha.php` 获取挑战图。
2. 用户拖动后调用 `/api/verify.php`。
3. 成功返回 `ticket`。
4. 业务后端提交关键操作时，把 `captcha_ticket + app_id` 发到 `/api/protected-action.php`（或在你的后端直接用 `SliderCaptcha::consumeTicket` 校验）。

## 三、前端接入（HTML 直接接入，推荐）

```html
<link rel="stylesheet" href="https://your-captcha-domain/assets/slider-captcha.css" />
<div id="captcha-box"></div>
<script src="https://your-captcha-domain/assets/slider-captcha.js"></script>
<script>
  // 从后台创建应用拿到
  const APP_ID = 'app_xxx'
  const APP_KEY = 'sk_xxx'
  const CAPTCHA_SCENE = 'login_submit'
  const makeActionNonce = () => `act_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`
  let currentActionNonce = makeActionNonce()

  const widget = new window.SliderCaptchaWidget({
    el: '#captcha-box',
    apiCaptcha: 'https://your-captcha-domain/api/captcha.php',
    apiVerify: 'https://your-captcha-domain/api/verify.php',
    apiTextCaptcha: 'https://your-captcha-domain/api/text-captcha.php',
    apiTextVerify: 'https://your-captcha-domain/api/text-verify.php',
    apiImageCaptcha: 'https://your-captcha-domain/api/image-captcha.php',
    apiImageVerify: 'https://your-captcha-domain/api/image-verify.php',
    apiInvisibleVerify: 'https://your-captcha-domain/api/invisible-verify.php',
    apiSign: 'https://your-captcha-domain/api/sign.php',
    appKey: APP_KEY,
    // 生产建议留空，走 apiSign 后端签名
    appSecret: '',
    transport: 'inline',
    // 弹窗验证（默认 true）
    popup: true,
    // 弹窗样式：'float' 浮动式（不遮蔽页面）| 'modal' 全屏式（带遮罩）
    popupMode: 'float',
    // 验证方式：slider | text | image | invisible | auto(默认随机)
    verifyType: 'auto',
    // 业务场景标识：同一 ticket 只能用于同一 scene
    scene: CAPTCHA_SCENE,
    // 业务动作 nonce：建议每次业务动作生成一个新值
    actionNonce: () => currentActionNonce,
    triggerText: '点击弹出验证码'
  });

  // 在提交业务前拿 ticket
  function getCaptchaTicket() {
    return widget.getTicket();
  }

  async function submitBiz() {
    const ticket = widget.getTicket();
    if (!ticket) {
      alert('请先完成验证码');
      return;
    }

    // 建议提交到你自己的业务后端
    const resp = await fetch('/your-api/submit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        captcha_ticket: ticket,
        app_id: APP_ID,
        captcha_scene: CAPTCHA_SCENE,
        captcha_action_nonce: currentActionNonce
      })
    });

    if (data.ok) {
      currentActionNonce = makeActionNonce()
    }

    const data = await resp.json();
    console.log(data);
  }
</script>
```

### 弹窗模式说明（`popupMode`）

- `float`（默认）：浮动式，面板贴近触发按钮出现，不遮蔽页面其他区域，适合表单内验证。
- `modal`：全屏式，带遮罩层，适合需要用户聚焦完成验证的流程。

示例：

```js
const widget = new window.SliderCaptchaWidget({
  el: '#captcha-box',
  appKey: APP_KEY,
  popup: true,
  popupMode: 'modal' // 切换为全屏模式
})
```

### 跨域接入注意事项

- 你的业务页面域名必须加入该应用 `allowed_origin`（支持多域名）。
- 否则会返回：`来源站点不被允许`。
- 不要把 `app_secret` 放到前端代码里。

## 四、后端接入（推荐）

### 1) 业务后端提交前校验

业务请求中带上：

- `captcha_ticket`
- `app_id`
- `captcha_scene`（建议必传，例如 `login_submit` / `pay_submit`）
- `captcha_action_nonce`（建议必传，针对单次业务动作生成）

然后在你的业务后端调用验证码服务：

```http
POST https://your-captcha-domain/api/protected-action.php
Content-Type: application/x-www-form-urlencoded

captcha_ticket=xxx&app_id=app_xxx
```

返回 `ok=true` 再继续业务逻辑。

> 强烈建议每个关键动作使用不同 `captcha_scene`，并为每次动作生成唯一 `captcha_action_nonce`，服务端应拒绝已用过的 `action_nonce`。

### 验证方式参数（`verifyType`）

- `slider`：滑块拼图验证
- `text`：文字点选验证
- `image`：图片选择验证
- `invisible`：无感人机验证
- `auto` 或不传：随机验证（默认）

### 2) 同项目内直连校验（PHP）

如果验证码系统和业务系统在同一代码仓，可以直接调用：

```php
<?php
require_once __DIR__ . '/src/bootstrap.php';

$ticket = (string)($_POST['captcha_ticket'] ?? '');
$appId = (string)($_POST['app_id'] ?? '');

if (!SliderCaptcha::consumeTicket($ticket, $appId)) {
    http_response_code(403);
    exit('captcha invalid');
}
```

## 五、跨语言项目示例

### Node.js（Express）

```js
import fetch from 'node-fetch'

async function verifyCaptcha(ticket, appId) {
  const params = new URLSearchParams({ captcha_ticket: ticket, app_id: appId })
  const resp = await fetch('https://your-captcha-domain/api/protected-action.php', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: params.toString()
  })
  const data = await resp.json()
  return data.ok === true
}
```

### Java（Spring）

```java
// 伪代码
String body = "captcha_ticket=" + URLEncoder.encode(ticket, UTF_8)
    + "&app_id=" + URLEncoder.encode(appId, UTF_8);
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://your-captcha-domain/api/protected-action.php"))
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();
```

## 六、安全建议（接入方）

1. 不要在前端暴露 `app_secret`。
2. 业务接口必须在服务端二次校验 `ticket`。
3. 一次业务请求只允许消费一次 `ticket`。
4. 登录/支付类接口建议叠加业务侧限流。
5. 前后端都记录失败日志，便于风控排查。

## 七、排错清单

- 报“应用身份不匹配”：`app_key`/`app_id` 混用了。
- 报“来源站点不被允许”：检查应用 `allowed_origin` 配置。
- 报“签名无效”：检查 `apiSign` 是否使用正确 `app_secret`。
- 报“请先完成滑动验证”：`ticket` 为空或已过期/已消费。
- 点击没反应或总失败：确认已引入 `slider-captcha.js` 且业务域名在 `allowed_origin` 白名单。
