@cikian/captcha-web

CikVerify 浏览器封装:框架无关 createCaptcha(),支持原生 JS / Vue 2 / Vue 3 / Nuxt / React / Angular
Install
npm install @cikian/captcha-web
Latest Version:1.0.0
Vue:2 · 3
License:Proprietary
Last Updated:Oct 19, 2026
Links: npm
Maintainer: cikian

@cikian/captcha-web

CikVerify 官方 浏览器端封装,包装官方 tac 静态资源。

发布/使用约束:线上只允许通过 npm 依赖使用本包。发布包只包含编译后的 dist/ 与生产压缩后的 tac/,不包含前端源码;请不要复制、反编译或修改 tac 源码。可定制项只有本文档列出的配置(主题、文案、URL、容器等)。

  • 框架无关:createCaptcha(options) → { open(): Promise<string>, destroy(), setLocale(), isOpen }
  • 适配 Vue 3(useCaptcha / CaptchaPlugin)与 Vue 2(Vue.use(CaptchaPlugin))
  • 兼容原生 JS / React / Angular / Nuxt(<ClientOnly>)
  • 成功从下游后端 verify 响应 res.data.ticket 取票
  • 失败自动 reload;关闭默认 reject({ reason: 'closed' })
  • SSR 安全:模块导入阶段不访问 window,提供 isSupported()
  • 主题透传、内置中英文文案(英文需显式传入)、自定义 messages、ESC 关闭、遮罩关闭、焦点回收、事件清理
  • 不引入任何统计脚本、不硬编码域名

安装

pnpm add @cikian/captcha-web

vue 为 optional peer dependency,只有使用 @cikian/captcha-web/vue2 或 /vue3 子入口时才需要,且不会被本包打包。

准备 tac 静态资源

@cikian/captcha-web 默认已内置官方 CDN:

https://cdn.cap.cikian.cn/assets/tac/

正常情况下无需复制任何文件,直接安装依赖、调用 createCaptcha() 即可。

可选:自托管 / 自有 CDN

默认配置不暴露该参数;只有需要覆盖默认 CDN 时才传 assetBaseUrl:

createCaptcha({ assetBaseUrl: 'https://cdn.example.com/tac' })

或使用包内生产 tac:

cp -r node_modules/@cikian/captcha-web/tac public/tac
createCaptcha({ assetBaseUrl: '/tac' })

tac 为生产压缩产物,随包发布仅用于自托管兜底,禁止修改。

内部维护者重新构建(仅维护者)

captcha-web-sdk 是内部源码工程,不对使用方开放。维护者修改验证码交互后:

cd captcha-web-sdk
npm install
npm run buildprod          # 生产压缩构建
cd ../packages/captcha-web
npm run sync:tac           # 同步为包内生产 tac(重命名为 js/tac.js)
npm run check:tac          # 校验压缩、无 sourceMap、无 console

使用方不需要、也不应该改 captcha-web-sdk;每次发布只带编译产物。

方式 C:手动引入 / 已有 tac

如果页面已经通过 <script> 引入了 tac,封装层会直接复用(不再注入):

<link rel="stylesheet" href="https://cdn.example.com/tac/css/tac.css" />
<script src="https://cdn.example.com/tac/js/tac.js"></script>

如果自备旧版兼容资源,也可以使用 load.min.js(暴露 window.loadTAC/window.initTAC);官方 tac 包不再包含该文件:

<script src="https://cdn.example.com/tac/load.min.js"></script>

原生 JS

<script type="module">
  import { createCaptcha } from '@cikian/captcha-web';

  const captcha = createCaptcha({
    challengeUrl: '/api/captcha/challenge', // 下游项目自己的接口(同源)
    verifyUrl: '/api/captcha/verify',
    type: 'SLIDER',       // 可选,缺省用服务端 defaultType
    theme: { logoUrl: '/logo.png', moveTrackMaskBgColor: '#FF4D6D' },
    onError: (error) => console.warn('[captcha]', error.reason, error.message),
  });

  document.querySelector('#login').addEventListener('click', async () => {
    try {
      const ticket = await captcha.open(); // 用户完成验证后 resolve
      await fetch('/api/login', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ captchaTicket: ticket }),
      });
    } catch (error) {
      if (error.reason === 'closed') return; // 用户取消
      // 其他错误:资源加载失败/超时/后端未签发 ticket 等
    }
  });
</script>

国际化(i18n)

弹窗文案默认中文;宿主通过 messages 配置自己的翻译,不需要改前端源码。未配置或缺少的字段自动回退中文。

import { createCaptcha } from '@cikian/captcha-web';

const captcha = createCaptcha({
  challengeUrl: '/api/captcha/challenge',
  verifyUrl: '/api/captcha/verify',
  locale: 'en-US', // 仅影响弹窗 lang/aria-label;文案由 messages 决定
  messages: {
    // 没写的字段自动使用中文默认值
    sliderTitle: 'Drag the slider to fit the puzzle',
    tipsSuccess: 'Verified in %s s',
  },
});

宿主翻译文件(zh-CN.json / en-US.json 等)建议按字段组织:

{
  "captcha": {
    "tipsSuccess": "验证成功,耗时%s秒",
    "tipsError": "验证失败,请重新尝试!",
    "tipsExpired": "验证码已失效,请重新验证",
    "tipsNetworkError": "网络异常,请稍后重试",
    "sliderTitle": "拖动滑块完成拼图",
    "concatTitle": "拖动滑块完成拼图",
    "rotateTitle": "拖动滑块完成拼图",
    "imageClickTitle": "请依次点击:",
    "imageClickConfirm": "确定",
    "powTitle": "安全验证",
    "powLoading": "正在验证中...",
    "disabledTitle": "验证码已关闭",
    "closeTitle": "关闭验证码",
    "refreshTitle": "刷新验证码",
    "overlayLabel": "安全验证"
  }
}

以 vue-i18n 为例:

const { t, te, locale } = useI18n();

function buildCaptchaMessages() {
  const pick = (key: string) =>
    te(`captcha.${key}`) ? t(`captcha.${key}`) : undefined; // 缺失返回 undefined
  return {
    tipsSuccess: pick('tipsSuccess'),
    tipsError: pick('tipsError'),
    tipsExpired: pick('tipsExpired'),
    tipsNetworkError: pick('tipsNetworkError'),
    sliderTitle: pick('sliderTitle'),
    concatTitle: pick('concatTitle'),
    rotateTitle: pick('rotateTitle'),
    imageClickTitle: pick('imageClickTitle'),
    imageClickConfirm: pick('imageClickConfirm'),
    powTitle: pick('powTitle'),
    powLoading: pick('powLoading'),
    disabledTitle: pick('disabledTitle'),
    closeTitle: pick('closeTitle'),
    refreshTitle: pick('refreshTitle'),
    overlayLabel: pick('overlayLabel'),
  };
}

const captcha = createCaptcha({
  challengeUrl: '/api/captcha/challenge',
  verifyUrl: '/api/captcha/verify',
  locale: locale.value,
  messages: buildCaptchaMessages(),
});

watch(locale, (value) => {
  captcha.setLocale(value, buildCaptchaMessages()); // 下一次 open() 生效
});
  • 宿主没有 captcha.* 翻译 → messages 为 undefined → 中文默认。
  • 宿主只翻译部分字段 → 未翻译字段 → 中文默认。
  • 需要英文但不想自己写 → 显式传入内置英文:
    import { enUSMessages } from '@cikian/captcha-web';
    createCaptcha({ messages: enUSMessages });
    
  • setLocale(locale) 不传 messages 会保留上次文案;恢复默认中文用 setLocale('zh-CN', {})。
  • theme.i18n 仍可用于覆盖 tac 原始键(高级用法),优先级最高。

内置字段(CaptchaMessages):

字段 说明
tipsSuccess 成功提示,%s 替换耗时秒数
tipsError 普通失败
tipsExpired 验证码失效
tipsNetworkError 网络异常
sliderTitle / concatTitle / rotateTitle 滑块/拼接/旋转标题
imageClickTitle / imageClickConfirm 点选标题/确定按钮
powTitle / powLoading 工作量证明标题/进行中
disabledTitle 不可用提示
closeTitle / refreshTitle 关闭/刷新按钮提示
overlayLabel 弹窗 aria-label

Vue 3

// main.ts
import { createApp } from 'vue';
import { CaptchaPlugin } from '@cikian/captcha-web/vue3';
import App from './App.vue';

createApp(App)
  .use(CaptchaPlugin, {
    challengeUrl: '/api/captcha/challenge',
    verifyUrl: '/api/captcha/verify',
  })
  .mount('#app');
<script setup lang="ts">
import { useCaptcha } from '@cikian/captcha-web/vue3';

const captcha = useCaptcha(); // 复用插件提供的全局实例

async function sendCode() {
  const ticket = await captcha.open();
  await fetch('/api/auth/send-code', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ captchaTicket: ticket }),
  });
}
</script>
  • 选项式 API 可用 this.$captcha.open()(插件已注入 ComponentCustomProperties 类型)
  • 传入 options 会创建独立实例,并在组件 scope 销毁时自动 destroy(): const captcha = useCaptcha({ type: 'ROTATE' })

Vue 2

// main.js
import Vue from 'vue';
import CaptchaPlugin from '@cikian/captcha-web/vue2';

Vue.use(CaptchaPlugin, {
  challengeUrl: '/api/captcha/challenge',
  verifyUrl: '/api/captcha/verify',
});
// 组件内
export default {
  methods: {
    async sendCode() {
      const ticket = await this.$captcha.open();
      // 提交 ticket
    },
  },
};

Nuxt 3

@cikian/captcha-web 是 SSR 安全的(导入不访问 window),但 open() 必须在浏览器执行。 组件用 <ClientOnly> 包裹,或只在事件回调中调用:

<template>
  <ClientOnly>
    <button @click="openCaptcha">获取验证码</button>
  </ClientOnly>
</template>

<script setup lang="ts">
import { createCaptcha } from '@cikian/captcha-web';

const captcha = createCaptcha({
  challengeUrl: '/api/captcha/challenge',
  verifyUrl: '/api/captcha/verify',
});

async function openCaptcha() {
  const ticket = await captcha.open();
  await $fetch('/api/auth/send-code', { method: 'POST', body: { captchaTicket: ticket } });
}
</script>

Nuxt 默认直接使用官方 CDN,无需 public/tac;如需自托管,再设置 assetBaseUrl。 服务端接口请使用 @cikian/captcha-client 的 createH3Handlers(Nitro/h3)。

React / Angular / 其他框架

直接使用核心 API(无框架依赖):

import { createCaptcha, isSupported } from '@cikian/captcha-web';

const captcha = createCaptcha({ challengeUrl: '/api/captcha/challenge' });
// 在表单按钮点击事件里 await captcha.open()
// 组件卸载时调用 captcha.destroy()

API

createCaptcha(options) => CaptchaInstance

字段 类型 默认值 说明
challengeUrl string /api/captcha/challenge 下游项目自己的 challenge 接口(同源)
verifyUrl string /api/captcha/verify 下游项目自己的 verify 接口
type string 服务端 defaultType SLIDER / ROTATE / CONCAT / WORD_IMAGE_CLICK / POW
locale string zh-CN 弹窗 lang/aria-label;不自动切换文案,文案由 messages 决定
messages object — 宿主翻译;缺失字段回退中文
assetBaseUrl string 官方 CDN 仅在自托管/私有 CDN 时覆盖
tacDir string — 已废弃,等价于 assetBaseUrl
theme object — 透传 tac style:logoUrl / bgUrl / btnUrl / moveTrackMaskBgColor / moveTrackMaskBorderColor / i18n
timeout number 0 整体超时(ms);0 表示不限制用户操作时间
loader auto | load | tac tac tac 直接加载 js/tac.min.js(失败回退 tac.js);auto/load 兼容旧 load.min.js
loadUrl string {assetBaseUrl}/load.min.js 显式覆盖 load.min.js
tacJsUrl string {assetBaseUrl}/js/tac.min.js 显式覆盖 tac.js;默认自动回退 js/tac.js
tacCssUrl string {assetBaseUrl}/css/tac.css 显式覆盖 tac.css
headers object {} challenge/verify 额外请求头(如 CSRF token)
resolveOnClose boolean false 用户关闭时 resolve '' 而非 reject
closeOnMaskClick boolean true 点击遮罩关闭
closeOnEscape boolean true ESC 关闭
container string | HTMLElement document.body 自定义挂载父元素
onError (error) => void — 致命错误回调(普通 verify 失败自动 reload,不触发)
scriptLoader / styleLoader function — 高级/测试:自定义资源加载器

返回实例:

成员 说明
open() 打开验证码;成功 resolve ticket。重复调用返回同一个 Promise
destroy() 立即关闭、清理 DOM/事件/焦点并中止进行中的 open()
isOpen 是否正在加载/展示(只读)

资源加载模式

  • auto(默认):优先复用页面已有的 window.TAC 或 window.loadTAC/initTAC; 否则先尝试 load.min.js,失败后回退到 js/tac.js + css/tac.css
  • load:只使用 load.min.js(window.loadTAC,兼容旧版 window.initTAC), SDK 会以 (assetBaseUrl, config, style) 调用它
  • tac:只注入 js/tac.js + css/tac.css,使用 new window.TAC(config, style).init()

脚本重复注入会复用同一份(模块级缓存);加载失败会 reject { reason: 'resource-load', message }。

成功 / 失败 / 关闭

  • 成功:verify 响应 code === 200 且 data.ticket 存在 → resolve(ticket)
  • 失败:validFail(4000/4001/4290/网络错误等)自动 reloadCaptcha(),不 reject
  • 缺少 ticket:code === 200 但无 data.ticket → reject { reason: 'ticket-missing' } (说明后端未使用 @cikian/captcha-client 适配器签发 ticket)
  • 用户关闭:默认 reject { reason: 'closed', trigger: 'button' | 'escape' | 'mask' }; resolveOnClose: true 时 resolve ''
  • 超时 / 销毁:{ reason: 'timeout' } / { reason: 'destroyed' }
  • SSR 调用:{ reason: 'unsupported' }

无障碍(a11y)

  • 遮罩带 role="dialog"、aria-modal="true"、aria-label="安全验证"
  • 打开时焦点移入验证码容器,关闭后恢复到触发元素
  • ESC 关闭(closeOnEscape: false 可关)
  • Tab 焦点在弹层内循环,避免焦点逃逸

与后端配合

浏览器只访问 下游项目自己的同源接口(challengeUrl / verifyUrl), 永远不直接访问 captcha 服务。后端使用 @cikian/captcha-client 完成 HMAC 签名与 ticket 签发:

浏览器 ──▶ /api/captcha/challenge ──HMAC──▶ captcha 服务
浏览器 ──▶ /api/captcha/verify    ──HMAC──▶ captcha 服务
                              └─ 200:签发 ticket(Redis 120s)──▶ 浏览器
浏览器把 ticket 随业务请求提交,后端 consumeTicket 后执行业务

发布说明

  • version / publishConfig.access / files / exports / sideEffects 已在 package.json 中配置;发布包只含 dist/ + 生产压缩后的 tac/,不含源码。

  • 发布前必须重新同步并校验生产 tac 资源:

    cd captcha-web-sdk && npm run buildprod
    cd ../packages/captcha-web
    npm run sync:tac
    npm run check:tac       # 无 sourceMap / 无 console / 已压缩
    npm run build
    cd ..
    pnpm -r test
    pnpm --filter @cikian/captcha-web publish --access public
    
  • captcha-web-sdk 的 package.json 已设置 "private": true,不会被 npm 发布; 使用方只允许依赖 @cikian/captcha-web,不允许复制/修改前端源码。

安全说明

  • 不引入统计脚本,不硬编码域名
  • ticket 只应通过 HTTPS 同源提交;后端必须一次性消费
  • onError / 控制台不输出 track/body
  • 静态资源建议走自有 CDN 并开启长期缓存(文件名带 hash 时可 immutable)

License

暂未指定(UNLICENSED),待项目方补充。

Related Packages

@zixinit/vue-captcha

Extensible Vue 3 image and slider CAPTCHA components with server adapters and SS...

732 0
@johnnyguan/vue-captcha-code

vue captcha code

2,470 0
vitest-browser-vue

Render Vue components in Vitest Browser Mode

5,213,984 79

Version History

Version Released Vue License
1.0.0 ^2.7.0 || ^3.2.0 Proprietary
0.1.0 ^2.7.0 || ^3.2.0 Proprietary
Pre-releases (1)
Version Released License
0.0.0-stage