@zixinit/vue-captcha
v0.1.2
Published
Extensible Vue 3 image and slider CAPTCHA components with server adapters and SSR support.
Maintainers
Readme
@zixinit/vue-captcha
可扩展的 Vue 3 验证码组件,包含图片验证码与滑块拼图。不依赖 UI 框架,支持 SSR、键盘操作、触屏、过期处理和请求取消。
安装
pnpm add @zixinit/vue-captcha需要 Vue 3.5+,提供 ESM 和 TypeScript 声明。所有入口均需在应用中引入一次样式:
import '@zixinit/vue-captcha/style.css'Nuxt 可将上述样式路径加入 nuxt.config.ts 的 css 数组。组件仅在客户端挂载后加载挑战;无需全局插件或 Pinia。
图片验证码
<script setup lang="ts">
import { ref } from 'vue'
import { ImageCaptcha } from '@zixinit/vue-captcha/image'
import type { ChallengeLoader, ImageChallenge } from '@zixinit/vue-captcha/core'
const challengeId = ref('')
const code = ref('')
const load: ChallengeLoader<ImageChallenge> = async ({ signal }) => {
const response = await fetch('/api/captcha/image', { signal })
if (!response.ok) throw new Error('加载失败')
return response.json()
}
function invalidate() {
challengeId.value = ''
code.value = ''
}
</script>
<template>
<input v-model="code" aria-label="验证码" autocomplete="off">
<ImageCaptcha :load="load" @loaded="challengeId = $event.id" @invalidated="invalidate" />
</template>服务端返回 { id, imageUrl, expiresAt? }。expiresAt 是 Unix 毫秒时间戳。业务提交时将 challengeId 与用户输入一起交给服务端校验;组件不持有正确答案。
滑块拼图
<script setup lang="ts">
import { ref } from 'vue'
import { SliderCaptcha } from '@zixinit/vue-captcha/slider'
import type { ChallengeLoader, SliderChallenge, SliderVerifier } from '@zixinit/vue-captcha/core'
const token = ref('')
const load: ChallengeLoader<SliderChallenge> = async ({ signal }) => {
const response = await fetch('/api/captcha/slider', { signal })
if (!response.ok) throw new Error('加载失败')
return response.json()
}
const verify: SliderVerifier = async (answer, { signal }) => {
const response = await fetch('/api/captcha/verify', {
method: 'POST', signal, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(answer),
})
if (!response.ok) throw new Error('验证失败')
return response.json()
}
</script>
<template>
<SliderCaptcha :load="load" :verify="verify"
@success="token = $event.token" @invalidated="token = ''" />
</template>挑战结构:
interface SliderChallenge {
id: string
backgroundUrl: string
pieceUrl: string
width: number
height: number
pieceWidth: number
pieceHeight: number
pieceTop: number
expiresAt?: number
}尺寸与 pieceTop 使用原始图片坐标单位,组件会按显示宽度缩放。独立拼块图片不应包含整个背景画布。提交参数为 { challengeId, offsetX, durationMs },offsetX 为拼块左边缘在原始背景中的横坐标;成功返回 { token, expiresAt? }。没有有效 token 不触发成功事件。刷新、重置、禁用、失败和过期均触发 invalidated,消费方应清空已保存的凭证。
服务端负责生成挑战、验证位置、控制尝试次数与有效期、签发并一次性消费凭证。durationMs 等客户端数据不可作为可信依据。业务接口必须核验 token,不能信任浏览器的成功事件。
公共 API
| 属性 | 说明 |
| --- | --- |
| load | 必填异步加载函数,接收 { signal } |
| verify | 仅滑块必填,接收答案与 { signal } |
| autoLoad | 默认 true;关闭后通过组件 ref 的 refresh() 加载 |
| disabled | 默认 false;禁用时取消请求并使当前挑战失效 |
| messages | Partial<CaptchaMessages>,覆盖默认中文文案和图片描述 |
公共事件:loaded(challenge)、invalidated()、expired()、error(Error);滑块额外提供 success(proof)。独立组件通过 ref 暴露 refresh(): Promise<void> 和 reset(): void,类型为 CaptchaHandle。
原生滑块支持方向键,左右按钮提供单击微调,确认按钮提交验证。视觉验证码仍需要业务提供非视觉验证路径,可通过 #alternative 插槽放置其他验证方式。
通过 .vc-captcha 上的 --vc-accent、--vc-surface、--vc-text、--vc-muted、--vc-border 等 CSS 变量调整主题,完整变量见 src/style.css。
扩展其他验证码
import { Captcha, createCaptchaRegistry } from '@zixinit/vue-captcha'
import ClickCaptcha from './ClickCaptcha.vue'
const registry = createCaptchaRegistry({ click: ClickCaptcha })
// 后续也可 registry.register('rotate', RotateCaptcha)<Captcha type="click" :registry="registry" :load="load" @success="onSuccess" />内置类型为 image 和 slider。注册表属于应用实例,不使用跨请求的全局注册状态;自定义组件决定自己的 props/events。统一入口转发属性、事件和 alternative 插槽,未知类型显示不支持提示,也可用 unsupported 插槽替换。需要命令式控制时直接使用独立组件。core 导出无 UI 的 createCaptchaController、类型和校验函数,可复用请求竞态与过期管理。
开发与发布
fnm use
pnpm install
make dev
make preflight打开 http://localhost:5173。Playground 的演示 API 仅由 Vite 开发插件提供,不包含在 npm 包中。它使用内存数据及演示凭证,图片验证码没有业务提交接口,不适用于生产防刷。build:demo 只生成静态页面,部署时需自行提供对应 API。
pnpm test:browser 运行浏览器交互测试(先 pnpm exec playwright install chromium)。发布流程见 deploy/DEPLOY.md。GitHub 保存完整源码,不执行其他包的文档镜像清理流程。
兼容已有 client-slider 协议
@zixinit/vue-captcha/client 导出浏览器端 createClientSliderChallenge({ imageUrl, challenge: { id, expiresAt }, signal })。返回 { challenge, checkPosition(offsetX) },可将 challenge 交给 SliderCaptcha;verify 适配器先调用 checkPosition,再调用应用的服务端验证接口并返回真实凭证。图片需要同源、data URL 或允许 CORS。
本地位置检查只提供交互反馈,不能抵御伪造请求,也不生成可信 token。该入口用于兼容已有的 client-slider 业务协议;服务端滑块直接使用服务端生成的 challenge。
原版客户端拼图
ClientSliderCaptcha(或 <Captcha type="client-slider">)内部使用 [email protected],保留原组件的弹窗、拖动、刷新与失败反馈。该依赖由本包统一管理,两端无需直接安装。组件仅在浏览器挂载后加载,适用于 Nuxt SSR。
<ClientSliderCaptcha v-model="visible" :imgs="images" :load="loadChallenge" :verify="verifyClient" @success="onProof" @invalidated="clearProof" />load 返回 { id, expiresAt? };verify 接收含 challengeId 的答案和 { signal },由业务适配器请求服务端验证,返回 { token, expiresAt? }。客户端拼图不提供服务端位移答案,兼容 SliderVerifier 的 offsetX、durationMs 固定为 0。本地拼图通过后才调用 verify;服务端凭证过期、关闭未完成验证或刷新都会清空旧状态。imgs 与原组件一致,接收图片 URL 数组。图片验证码、服务端拼图仍使用 ImageCaptcha、SliderCaptcha。
