npm package: @flexem/aj-captcha
Description: AJ-Captcha 前端 JS SDK，支持滑动拼图和点选文字验证码
Latest version: 1.0.3

# AJ-Captcha .NET 版
服务器域名： captcha.fbox360.com

行为验证码 C# WebAPI 实现，从 [anji-plus/captcha](https://gitee.com/anji-plus/captcha) Java 版翻译而来。

支持两种验证码类型：
- **blockPuzzle** — 滑动拼图（拖动滑块到缺口位置）
- **clickWord** — 点选文字（按顺序点击图片中指定的汉字）

---


## API 接口

所有接口均为 `POST`，`Content-Type: application/json`。

### POST /captcha/get — 获取验证码

**请求体**

```json
{
  "captchaType": "blockPuzzle"
}
```

`captchaType` 可选值：`blockPuzzle` / `clickWord` / `default`（默认使用 blockPuzzle）

**响应（blockPuzzle）**

```json
{
  "repCode": "0000",
  "repMsg": "success",
  "repData": {
    "token": "a1b2c3d4...",
    "secretKey": "AbCdEfGh12345678",
    "originalImageBase64": "iVBORw0KGgo...",
    "jigsawImageBase64": "iVBORw0KGgo..."
  }
}
```

**响应（clickWord）**

```json
{
  "repCode": "0000",
  "repMsg": "success",
  "repData": {
    "token": "a1b2c3d4...",
    "secretKey": "AbCdEfGh12345678",
    "originalImageBase64": "iVBORw0KGgo...",
    "wordList": ["验", "码", "点"]
  }
}
```

---

### POST /captcha/check — 校验用户操作

**请求体（blockPuzzle）**

```json
{
  "captchaType": "blockPuzzle",
  "token": "a1b2c3d4...",
  "pointJson": "<AES加密后的坐标JSON>"
}
```

`pointJson` 是前端用 `secretKey` 对坐标对象 `{"x":200,"y":5}` 做 AES/ECB/PKCS7 加密后的 Base64 字符串。

**请求体（clickWord）**

```json
{
  "captchaType": "clickWord",
  "token": "a1b2c3d4...",
  "pointJson": "<AES加密后的坐标数组JSON>"
}
```

`pointJson` 解密后为点击坐标数组：`[{"x":85,"y":34},{"x":129,"y":56},{"x":233,"y":27}]`

**响应（校验通过）**

```json
{
  "repCode": "0000",
  "repMsg": "success",
  "repData": {
    "token": "a1b2c3d4...",
    "result": true
  }
}
```

---

### POST /captcha/internal/verify-v1 — 服务端二次校验

由**业务后端**调用，用于最终确认前端的验证结果有效。
**请求头**
带上 "x-secret" ,值的话问服务端后台人员单独给

**请求体**

```json
{
  "captchaVerification": "<二次校验值>"
}
```

`captchaVerification` 的计算方式见下文"两阶段验证协议"章节。

**响应（校验通过）**

```json
{
  "repCode": "0000",
  "repMsg": "success"
}
```

---

### 错误码

| repCode | 含义 |
|---------|------|
| `0000`  | 成功 |
| `6110`  | 验证码已失效（token 过期或已使用），请重新获取 |
| `6111`  | 坐标校验失败（位置不对） |
| `6112`  | 获取验证码失败（底图未初始化） |

---

## 工作原理

### 整体流程

```
前端                      验证码服务                    业务后端
 │                            │                            │
 │─── POST /captcha/get ─────>│                            │
 │<── token + secretKey + 图  │                            │
 │    base64底图              │                            │
 │                            │                            │
 │  用户完成交互              │                            │
 │  （拖滑块 / 点文字）       │                            │
 │                            │                            │
 │─── POST /captcha/check ───>│                            │
 │    token + AES加密坐标     │                            │
 │<── result: true            │                            │
 │                            │                            │
 │  前端计算 captchaVerification                           │
 │                            │                            │
 │─────────────── 携带 captchaVerification 提交业务表单 ──>│
 │                            │                            │
 │                            │<── POST /captcha/verify ───│
 │                            │    captchaVerification     │
 │                            │──> repCode: 0000 ─────────>│
 │                            │                            │
 │<──────────────────── 业务响应（登录成功等） ───────────│
```

---

### 两阶段验证协议

防止重放攻击的核心机制：

**阶段一：get + check**

1. `GET /captcha/get`
   - 服务端生成随机坐标 `(x, y)` 和 AES 密钥 `secretKey`
   - 将 `token → {x, y, secretKey}` 存入缓存，**有效期 2 分钟**
   - 返回给前端：`token`、`secretKey`、图片 base64

2. `POST /captcha/check`
   - 前端用 `secretKey` 对坐标加密后发送
   - 服务端从缓存取出坐标，**立即删除**（一次性使用，防重放）
   - 验证坐标是否匹配（blockPuzzle 允许 ±5px 误差）
   - 验证通过后，计算：
     ```
     value = AES_Encrypt(token + "---" + pointJson, secretKey)
     ```
   - 将 `"second-{value}" → token` 存入缓存，**有效期 3 分钟**
   - 返回 `result: true`

**阶段二：verify**

3. 前端（或业务后端）计算 `captchaVerification`：
   ```javascript
   // 前端用同样的 secretKey 计算（与服务端一致）
   captchaVerification = AES_Encrypt(token + "---" + pointJson, secretKey)
   ```

4. `POST /captcha/verify`
   - 服务端查找 `"second-{captchaVerification}"` 是否在缓存中
   - 找到则**立即删除**，返回成功（一次性使用）
   - 找不到则返回 `6110`

**安全保证：**
- token 一次性：check 后立即失效，防止重复提交
- secondKey 一次性：verify 后立即失效，防止重放
- captchaVerification 由 check 时的参数派生，无法伪造

---

### blockPuzzle 图片生成算法

```
原始底图 (310×155)          模板滑块图 (约56×56, 含透明通道)
      │                              │
      └──── 随机生成坐标 (x, y) ────┘
                  │
    遍历模板每个像素 (i, j)：
    ┌─────────────────────────────────────────┐
    │ 若模板像素 alpha > 0（非透明）:           │
    │   → 将原图 (x+i, y+j) 的像素复制到新滑块 │
    │   → 对原图该像素做 3×3 高斯模糊（挖洞）  │
    │                                         │
    │ 若当前像素与右邻/下邻透明性不同（边界）：  │
    │   → 新滑块该像素设为白色（描边）         │
    │   → 原图对应位置设为白色（轮廓提示）      │
    └─────────────────────────────────────────┘
                  │
      ┌───────────┴───────────┐
      │                       │
   返回原图（含模糊洞+白色轮廓）  返回新滑块（透明背景+原图像素）
```

高斯模糊算法（对应 Java `avgMatrix`）：
```
取目标像素周围 3×3 邻域的 6 个像素（跳过中间列）
avg_R = (R1+R2+R3+R4+R5+R6) / 8
avg_G = (G1+G2+G3+G4+G5+G6) / 8
avg_B = (B1+B2+B3+B4+B5+B6) / 8
```

---

### clickWord 文字生成算法

```
1. 加载随机点选底图
2. 随机选取 N 个不重复汉字（来自 500+ 常用汉字集）
3. 对每个汉字：
   - 按宽度分区随机放置坐标（避免重叠）
   - 随机颜色 RGB(1-255, 1-255, 1-255)
   - 随机旋转角度 [-45°, 45°]
   - 用 WenQuanZhengHei.ttf 字体绘制
4. 随机隐藏其中 1 个汉字（从 wordList 和校验列表中排除）
5. 在右下角绘制水印
6. 返回：底图 base64 + wordList（需点击的 N-1 个汉字，按顺序）
7. 缓存：N-1 个汉字的坐标，用于 check 时校验
```

---

### AES 加密规范

与前端交互时的加密参数：

| 项目 | 值 |
|------|-----|
| 算法 | AES |
| 模式 | ECB |
| 填充 | PKCS7 |
| 密钥长度 | 128 bit（16字节） |
| 密钥来源 | `GET /captcha/get` 返回的 `secretKey` |
| 编码 | Base64 |

**blockPuzzle `pointJson` 加密内容：**
```json
{"x": 215, "y": 5}
```

**clickWord `pointJson` 加密内容：**
```json
[{"x": 85, "y": 34}, {"x": 129, "y": 56}, {"x": 233, "y": 27}]
```

---

## 配置项

`appsettings.json` 中 `Captcha` 节：

| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| `Type` | `default` | 验证码类型：`default`/`blockPuzzle`/`clickWord` |
| `CacheType` | `local` | 缓存类型：`local`（内存）|
| `WaterMark` | `我的水印` | 右下角水印文字 |
| `WaterFont` | `WenQuanZhengHei.ttf` | 水印字体 |
| `FontType` | `WenQuanZhengHei.ttf` | 点选验证码字体 |
| `FontSize` | `25` | 点选文字大小（px） |
| `SlipOffset` | `5` | 滑动验证允许误差（px） |
| `AesStatus` | `true` | 是否开启 AES 坐标加密 |
| `InterferenceOptions` | `2` | 干扰滑块数量：0/1/2 |
| `ClickWordCount` | `4` | 点选文字总数（其中 1 个不参与校验） |
| `Jigsaw` | null | 自定义滑动验证底图目录（空=使用默认） |
| `PicClick` | null | 自定义点选验证底图目录（空=使用默认） |

---

## 项目结构

```
src/AjCaptcha.WebApi/
├── Configuration/
│   └── CaptchaOptions.cs          配置项映射类
├── Controllers/
│   └── CaptchaController.cs       HTTP 接口（get/check/verify）
├── Models/
│   ├── CaptchaVO.cs               请求/响应数据对象
│   ├── PointVO.cs                 坐标对象（x, y, secretKey）
│   └── ResponseModel.cs           统一响应格式
├── Services/
│   ├── ICaptchaService.cs         验证码服务接口
│   ├── ICaptchaCacheService.cs    缓存服务接口
│   ├── CaptchaServiceFactory.cs   按类型路由服务
│   └── Impl/
│       ├── AbstractCaptchaService.cs    check/verify 公共逻辑
│       ├── BlockPuzzleCaptchaService.cs 滑动拼图实现
│       ├── ClickWordCaptchaService.cs   点选文字实现
│       └── MemoryCaptchaCacheService.cs 内存缓存实现
├── Utils/
│   ├── AesUtil.cs      AES 加解密
│   ├── ImageUtils.cs   底图加载 + SkiaSharp 图片处理
│   ├── RandomUtils.cs  随机数/UUID/随机字符串
│   └── Md5Util.cs      MD5 工具
└── Resources/
    ├── defaultImages/
    │   ├── jigsaw/original/       滑动验证底图（7张）
    │   ├── jigsaw/slidingBlock/   滑块模板图（6张，带透明通道）
    │   └── pic-click/             点选验证底图（17张）
    └── fonts/
        └── WenQuanZhengHei.ttf    开源中文字体
```

---

## 扩展说明

### 接入 Redis 缓存

1. 添加 NuGet 包：`StackExchange.Redis`
2. 实现 `ICaptchaCacheService` 接口，使用 Redis 操作替换内存操作
3. 在 `Program.cs` 中替换注册：
   ```csharp
   // 替换这行
   builder.Services.AddSingleton<ICaptchaCacheService, MemoryCaptchaCacheService>();
   // 改为
   builder.Services.AddSingleton<ICaptchaCacheService, RedisCaptchaCacheService>();
   ```

> 多节点部署时**必须使用 Redis**，内存缓存只适合单节点。

### 使用自定义底图

将图片（PNG/JPG）放入任意目录，在 `appsettings.json` 中配置路径：

```json
{
  "Captcha": {
    "Jigsaw": "D:/myimages/jigsaw",
    "PicClick": "D:/myimages/pic-click"
  }
}
```

### 添加新的验证码类型

1. 继承 `AbstractCaptchaService`，实现 `Get()` 和 `ValidateCoordinates()`
2. 在 `Program.cs` 注册：
   ```csharp
   builder.Services.AddSingleton<ICaptchaService, MyNewCaptchaService>();
   ```

---

## 前端 JS SDK

零依赖，兼容 IE9+，支持 TypeScript。

### 安装

```bash
npm install @flexem/aj-captcha
```

或直接引入：

```html
<script src="aj-captcha.min.js"></script>
```

### TypeScript 导入

```typescript
// esModuleInterop: true
import AJCaptcha from '@flexem/aj-captcha';

// 或 CommonJS 风格
import AJCaptcha = require('@flexem/aj-captcha');
```

### 弹窗模式（推荐）

```javascript
AJCaptcha.popup({
  server: 'https://captcha.fbox360.com',
  captchaType: 'blockPuzzle', // 或 'clickWord'
  title: '请完成安全验证',
  lang: {
    sliderTip: '向右拖动滑块填充拼图',
    clickTip: '请依次点击：',
    refreshTitle: '刷新',
    checking: '验证中...',
    verifySuccess: '验证通过',
    verifyFail: '验证失败，请重试',
    getFail: '获取验证码失败',
    checkFail: '校验失败',
    parseFail: '解析响应失败',
    networkError: '网络错误: ',
    popupTitle: '请完成安全验证'
  },
  onSuccess: function (verification) {
    console.log('验证通过:', verification);
    // 将 verification 提交给业务接口做二次校验
  },
  onFail: function (err) {
    console.error('验证失败:', err);
  },
  onClose: function () {
    console.log('用户关闭弹窗');
  }
});
```

`lang` 中所有字段均可选，未传则使用默认中文文案。

### 内嵌模式

```javascript
var captcha = new AJCaptcha({
  server: 'https://captcha.fbox360.com',
  captchaType: 'clickWord',
  el: '#captcha-container', // 或传入 DOM 元素
  onSuccess: function (verification) {
    console.log(verification);
  }
});
captcha.render();
```

### API

| 方法 | 说明 |
|------|------|
| `AJCaptcha.popup(options)` | 弹窗验证，返回 `{ close, instance }` |
| `instance.render()` | 渲染验证码到 `el` |
| `instance.get(callback)` | 手动获取验证码数据 |
| `instance.check(pointData, callback)` | 手动校验坐标 |
| `instance.getCaptchaVerification()` | 获取加密后的验证凭证 |
| `AJCaptcha.AesUtil.encrypt(text, key)` | AES/ECB/PKCS7 加密工具 |