human-verify
v1.2.2
Published
人机验证库 - 行为分析验证
Downloads
430
Maintainers
Readme
HumanVerify 人机验证库
基于行为特征分析与全屏点击挑战的人机验证库,前后端分离架构。Vue 已打包进库,无需使用者安装 Vue。
包内容
human-verify/
├── frontend/
│ ├── human-verify.es.js # 前端 ES 模块(混淆,Vue 已打包)
│ ├── human-verify.umd.js # 前端 UMD 模块(混淆,Vue 已打包)
│ └── index.d.ts # 前端类型声明
├── backend/
│ ├── index.js # 后端验证库(混淆)
│ └── index.d.ts # 后端类型声明
├── README.md
└── package.json安装
npm install human-verify前端使用
组件模式(Vue 3 项目)
<script setup>
import { HumanVerify } from 'human-verify'
async function onVerified(result) {
const details = result.details
const pow = details?.pow
if (pow) {
// 使用者自行调用 pow-verify 接口完成最终验证
const verifyResp = await fetch('/api/verify/pow-verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: details.sessionId, nonce: pow.nonce })
})
const verifyResult = await verifyResp.json()
if (verifyResult.passed) {
console.log('最终验证通过')
}
}
}
</script>
<template>
<HumanVerify
challenge-url="/api/verify/challenge"
pow-challenge-url="/api/verify/pow-challenge"
@verified="onVerified"
@failed="onFailed"
/>
</template>函数模式
import { humanVerify } from 'human-verify'
const result = await humanVerify({
challengeUrl: '/api/verify/challenge',
powChallengeUrl: '/api/verify/pow-challenge',
onProgress: (progress, status) => {
console.log(`${progress}%: ${status}`)
}
})
if (result.passed && result.pow) {
// 使用者自行调用 pow-verify 接口完成最终验证
const verifyResp = await fetch('/api/verify/pow-verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId: result.sessionId,
nonce: result.pow.nonce
})
})
const verifyResult = await verifyResp.json()
console.log('最终验证结果:', verifyResult)
}自定义元素模式(纯 HTML)
<script src="/lib/human-verify.umd.js"></script>
<human-verify
challenge-url="/api/verify/challenge"
pow-challenge-url="/api/verify/pow-challenge"
onverify-id="my-callback"
></human-verify>
<script>
window.HumanVerifyCallbacks.set('my-callback', async (result) => {
const details = result.details
const pow = details?.pow
if (pow) {
// 使用者自行调用 pow-verify 接口
const verifyResp = await fetch('/api/verify/pow-verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: details.sessionId, nonce: pow.nonce })
})
const verifyResult = await verifyResp.json()
if (verifyResult.passed) {
console.log('验证通过,得分:', details.score)
}
}
})
</script>组件属性
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| challenge-url | string | '/api/verify/challenge' | 获取挑战接口 URL |
| pow-challenge-url | string | '/api/verify/pow-challenge' | 提交行为验证 + POW 挑战接口 URL |
| timeout | number | 30000 | 超时时间(ms) |
| onverify-id | string | — | 回调 ID,通过 window.HumanVerifyCallbacks.set(id, fn) 注册 |
组件事件
| 事件 | detail | 说明 |
|------|--------|------|
| verified | { passed, reason, details } | 行为验证通过 + POW 计算完成,pow 等信息在 details 中 |
| failed | { reason, details } | 验证失败,详细信息在 details 中 |
| progress | { progress, status } | 进度更新 |
返回结果
interface VerifyResult {
passed: boolean // 行为验证是否通过
score: number // 行为评分 (0-100)
sessionId: string // 会话 ID(供使用者调用 pow-verify 接口)
pow?: {
challenge: string
nonce: number // POW 计算结果(供使用者提交)
powTarget: number // POW 目标阈值
powDurationMs: number // POW 计算耗时(ms)
}
reason: string // 提示信息:仅返回"人机验证成功"或"人机验证失败"
details?: {
serverReason?: string // 组件模式下可能包含服务端原始失败原因(用于调试);函数模式不返回
}
}注意:
reason字段仅用于 UI 展示,始终返回固定文本(成功="人机验证成功",失败="人机验证失败")。函数模式不暴露details.serverReason,避免泄露具体是哪项校验失败(如"点击未命中挑战目标")被逆向利用;组件模式保留该字段仅供调试。
后端使用
import {
createSession,
verifySession,
generatePOWChallenge,
verifyPOWSolution,
detectAutomation,
analyzeBehavior,
configureSessionStore,
createRedisSessionStore
} from 'human-verify/backend'后端库为 ESM 格式,Node.js 18+。所有会话相关函数均为 async(返回 Promise),请使用
await调用。
会话存储(内存 / Redis 集群)
默认使用内存 Map(单实例)。部署到多实例/集群时,各实例内存互相隔离,
会话会"串"到其他节点导致验证失败。此时启用 Redis 存储,由调用方传入
Redis 操作函数(任意 Redis 客户端均可,如 ioredis、redis、云厂商 SDK):
import { configureSessionStore, createRedisSessionStore } from 'human-verify/backend'
// 不调用 configureSessionStore → 内存模式(默认)
// 调用后 → Redis 模式,多实例共享同一份会话
configureSessionStore(createRedisSessionStore({
// 例:ioredis
get: (key) => client.get(key),
set: (key, value, ttlMs) => client.set(key, value, 'PX', ttlMs),
del: (key) => client.del(key)
// 可选:keyPrefix 默认 'hv:session:'
}))
// 建议在服务启动时(创建任何会话前)调用一次configureSessionStore(store)— 切换会话存储,可在启动时调用一次createRedisSessionStore({ get, set, del, keyPrefix? })— 用 Redis 操作函数构造存储;会话过期交给 Redis 原生 TTL,防重放删除通过del同步到所有实例- 也可自定义实现
SessionStore(get/set/del)接入其他存储(如 Memcached、数据库)
函数列表
| 函数 | 参数 | 返回值 | 说明 |
|------|------|--------|------|
| createSession(options?) | { viewport?: { width, height } }(可选,前端上报的视口尺寸,缺省 1920×1080;挑战图按该尺寸 1:1 原样生成,不缩放不变形;最小边 <320 拒绝) | Promise<{ sessionId, publicKey, image, expiresAt }> | 创建会话,生成 RSA 密钥对;按视口尺寸生成全屏透明挑战图(data URL),目标图案/颜色/坐标随机,坐标仅保存在服务端,不下发 |
| verifySession(sessionId, encryptedData) | sessionId, 加密数据 | Promise<{ success, passed?, score?, reason?, error?, details? }> | 解密并校验点击位置是否命中服务端保存的目标 + 行为评分;分数阈值由会话级随机配置决定,客户端无法指定;details.serverReason 存原始原因 |
| generatePOWChallenge(sessionId) | sessionId | Promise<{ success, challenge?, powTarget?, estimatedIterations?, error? }> | 根据评分生成 POW 挑战 |
| verifyPOWSolution(sessionId, nonce) | sessionId, nonce | Promise<{ success, passed?, score?, error? }> | 验证 POW 结果,无论成功失败都销毁会话(防重放) |
| detectAutomation(fingerprint) | DeviceFingerprint | { isBot, reason } | 检测自动化工具 |
| analyzeBehavior(data, challengeResult, fingerprint?, weights?, threshold?, target?) | 行为数据, 挑战结果, ... | VerifyResponse | 分析行为数据(含点击位置与视口目标比对),reason 统一返回固定文本,details.serverReason 存原始原因 |
| configureSessionStore(store) | SessionStore | void | 切换会话存储(见上节),启动时调用一次 |
| createRedisSessionStore(opts) | { get, set, del, keyPrefix? } | SessionStore | 用 Redis 操作函数构造存储(见上节) |
示例
import express from 'express'
import { createSession, verifySession, generatePOWChallenge, verifyPOWSolution } from 'human-verify/backend'
const app = express()
app.use(express.json())
// 1. 创建验证会话(生成全屏挑战图)
app.post('/api/verify/challenge', async (req, res) => {
const session = await createSession({ viewport: req.body?.viewport })
res.json(session)
})
// 2. 行为验证 + POW 挑战
app.post('/api/verify/pow-challenge', async (req, res) => {
const { sessionId, encryptedData } = req.body
// 阈值由会话级随机配置决定,客户端无法指定
const result = await verifySession(sessionId, encryptedData)
if (!result.success) {
return res.status(400).json({ error: result.error })
}
if (!result.passed) {
// 验证失败,会话已销毁
return res.json({ passed: false, score: result.score, reason: result.reason, details: result.details })
}
// 验证通过,生成 POW 挑战(会话保留供 POW 验证)
const pow = await generatePOWChallenge(sessionId)
res.json({ passed: true, score: result.score, pow, reason: result.reason })
})
// 3. POW 验证(使用者自行调用)
app.post('/api/verify/pow-verify', async (req, res) => {
const { sessionId, nonce } = req.body
const result = await verifyPOWSolution(sessionId, nonce)
if (!result.success) {
return res.status(400).json({ error: result.error })
}
// 无论成功失败,会话都已销毁(防重放)
res.json({ passed: result.passed, score: result.score })
})
app.listen(3000)验证流程
前端 后端
│ │
│ POST /api/verify/challenge │
│ { viewport: { width, height } } │
│ ──────────────────────────────────────────► │ createSession()
│ ◄────────────────────────────────────────── │ → { sessionId, publicKey, image }
│ │ 目标坐标仅存服务端,不下发
│ │
│ 将 image 铺满屏幕(除目标图案外透明) │
│ 用户点击随机图案(形状/颜色每次不同) │
│ 采集指纹 + 行为数据 + 点击位置 │
│ Web Worker 加密(RSA + AES) │
│ │
│ POST /api/verify/pow-challenge │
│ { sessionId, encryptedData } │
│ ──────────────────────────────────────────► │ verifySession() → 校验点击位置
│ │ 命中视口目标 + 行为评分
│ │ (失败则销毁会话)
│ │ generatePOWChallenge()
│ ◄────────────────────────────────────────── │ → { passed, score, pow }
│ │
│ Web Worker 计算 POW(实时进度) │
│ → 得到 nonce │
│ │
│ 使用者自行调用 pow-verify │
│ POST /api/verify/pow-verify │
│ { sessionId, nonce } │
│ ──────────────────────────────────────────► │ verifyPOWSolution()
│ │ (销毁会话,防重放)
│ ◄────────────────────────────────────────── │ → { passed, score }安全机制
- 全屏挑战图 — 后端按前端上报的视口尺寸生成透明 PNG 铺满屏幕,目标图案(圆形/圆环/方形/菱形/五角星/十字/三角形)与颜色每次随机;目标坐标仅保存在服务端,不在接口中下发
- 防图像解码提取 — PNG 每行随机使用 None/Sub 滤波(对抗按滤波 0 解析的轻量解码器)、全屏低透明噪点、大范围极淡渐变云、低透明度假目标点、相对目标偏移的淡光晕,抬高"解码图像反推坐标"的自动化成本
- 点击位置强校验 — 前端点击后立即清除图片并把点击位置传回,服务端比对视口目标(容差 ≤60px),且要求点击位置与真实采集的点击一致(防伪造字段)
- 视口防盲猜 — 挑战图按上报视口 1:1 原样生成(手机等任意尺寸不缩放不变形);最小边 <320 的视口直接拒绝创建会话(真实设备视口最小边 ≥320,只有伪造请求会触发)——否则过小视口(如 200×200)的目标可达范围会被压缩到容差圆之内,攻击者直接提交视口中心即可 100% 命中
- IP 失败限流 — 同一 IP 5 分钟窗口内验证失败 ≥12 次即拒绝后续请求(429),配合视口校验让"盲猜重试"通道失效
- 防重放 — 会话一次性使用,验证即销毁,过期时间 5 分钟
- 双层加密 — RSA-OAEP-SHA256(2048 位)+ AES-256-GCM,Web Worker 中执行
- 工作量证明 — SHA-256 前导零位,难度 1-10 秒,Web Worker 计算
- 自动化检测 — Playwright / Puppeteer / Selenium / Headless 检测
- 设备指纹 — 100+ 采集项(Canvas / WebGL / Audio / Fonts / WebRTC)
- 代码混淆 — 控制流扁平化 / 死代码注入 / 字符串数组化
License
MIT
