@cikian/captcha-web
@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.cssload:只使用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
Extensible Vue 3 image and slider CAPTCHA components with server adapters and SS...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 1.0.0 | ^2.7.0 || | Proprietary | |
| 0.1.0 | ^2.7.0 || | Proprietary |
Pre-releases (1)
| Version | Released | License |
|---|---|---|
| 0.0.0-stage |