在 Vue 3 项目里接入验证码,很多人第一反应是找个滑块组件装上就完事。但真正上线后会发现,攻击者根本不碰你的页面,直接用脚本调接口,前端那套滑块形同虚设。验证码的本质是一道人机识别题,而反欺诈的本质是持续的行为信号分析,两者必须放在工程化架构里统一设计,才能形成有效防线。这篇文章从组件封装、行为采集、令牌管理、请求拦截四个层面,给出一套可以直接落地的方案。

一、验证码组件的工程化封装
先说组件层。无论你用的是极验、腾讯防水墙还是自研滑块,接入方式大同小异:加载一个外部 SDK,初始化拿到实例,用户完成验证后回调一个 token。问题在于,如果每个业务页面都自己写一遍加载和初始化逻辑,代码会迅速腐化。正确的做法是封装一个无感知的验证组件,把 SDK 加载、实例管理、失败重试全部收敛到内部。
组件设计上有两个关键点。第一,SDK 脚本要用动态加载,避免拖慢首屏;第二,验证过程对业务方应该是黑盒,业务方只关心最终拿到的 token。下面是一个典型的封装示例:
// composables/useCaptcha.js
import { ref, onUnmounted } from 'vue'
export function useCaptcha() {
const captchaReady = ref(false)
let captchaIns = null
// 动态加载第三方 SDK 脚本
function loadScript(url) {
return new Promise((resolve, reject) => {
if (window.__captchaSdk) return resolve()
const s = document.createElement('script')
s.src = url
s.onload = () => {
window.__captchaSdk = true
resolve()
}
s.onerror = reject
document.head.appendChild(s)
})
}
async function init() {
await loadScript('https://cdn.example-vendor.com/captcha.js')
captchaIns = new window.Captcha({
appId: import.meta.env.VITE_CAPTCHA_APPID,
mode: 'popup', // 弹出式验证
onSuccess: (token) => {
// 将 token 存入全局风控状态,供请求层使用
riskStore.setToken(token)
},
onFail: () => {
// 失败后自动换题,连续失败三次触发降级策略
retryCount++
if (retryCount >= 3) captchaIns.switchToSmsMode()
}
})
captchaReady.value = true
}
onUnmounted(() => captchaIns?.destroy())
return { captchaReady, init, verify: () => captchaIns?.verify() }
}这样封装后,业务组件里只需要一个 verify() 调用,完全不用关心 SDK 细节。另外一个容易被忽略的细节是降级策略:当第三方验证服务不可用时,要能切换到短信验证或邮箱验证,这个开关最好由后端下发配置,前端不要写死,否则验证服务一挂,注册登录全部瘫痪。
二、行为采集与设备指纹
验证码只是人机识别的一个瞬时判断,反欺诈需要的是持续的行为证据。鼠标移动轨迹、打字节奏、页面停留时长、设备环境信息,这些信号组合起来才能刻画出一个请求背后是真人还是机器。Vue 3 的组合式 API 提供了很干净的方式来做这件事。
轨迹采集的核心思路是:监听 pointermove 事件,按时间戳采样记录坐标,只保留最近一段时间的窗口数据,避免内存无限增长。机器生成的轨迹通常是匀速直线,而人类轨迹有明显的不规则抖动和加速减速,这些特征后端风控引擎会做算法分析:
// composables/useBehaviorTrack.js
import { onMounted, onUnmounted } from 'vue'
export function useBehaviorTrack() {
const track = []
let handler = null
onMounted(() => {
handler = (e) => {
// 采样限流:每 40ms 记录一个点足够还原轨迹特征
const now = Date.now()
const last = track[track.length - 1]
if (last && now - last.t < 40) return
track.push({ x: e.clientX, y: e.clientY, t: now })
// 只保留最近 10 秒的轨迹数据
if (track.length > 300) track.shift()
}
window.addEventListener('pointermove', handler, { passive: true })
})
onUnmounted(() => window.removeEventListener('pointermove', handler))
function getSnapshot() {
return {
track,
// 简易设备指纹:屏幕、时区、语言、硬件并发的哈希
fingerprint: fingerprintHash(),
ua: navigator.userAgent,
tz: Intl.DateTimeFormat().resolvedOptions().timeZone
}
}
return { getSnapshot }
}设备指纹要注意合规边界。采集硬件并发数、屏幕分辨率这些低敏感信息一般没问题,但涉及 Canvas 指纹、AudioContext 指纹时,必须遵守隐私法规在隐私政策中明示。指纹的定位在于关联:同一个设备换账号注册、同一设备批量登录,都是典型的欺诈信号,这些判断由服务端完成,前端只负责如实上报。
三、令牌管理与 Axios 拦截器联动
信号采集到了,怎么随请求发出去?推荐用 Pinia 建一个风控 store,统一管理验证码 token 的生命周期,再在 Axios 拦截器里自动注入。这样业务代码完全无感知,后面就算换验证码厂商,改动也只集中在拦截器和 store 两处。
服务端返回 403 或特定风控错误码时,拦截器应自动触发一次前端验证流程,验证通过后重放原请求。这个模式对用户来说是接近无感的,体验远好于直接报错。完整实现如下:
// stores/risk.js
import { defineStore } from 'pinia'
export const useRiskStore = defineStore('risk', {
state: () => ({ captchaToken: '' }),
actions: {
setToken(t) { this.captchaToken = t }
}
})
// api/request.js
import axios from 'axios'
import { useRiskStore } from '@/stores/risk'
import { useCaptcha } from '@/composables/useCaptcha'
const instance = axios.create({ baseURL: '/api' })
instance.interceptors.request.use((config) => {
const risk = useRiskStore()
if (risk.captchaToken) {
config.headers['X-Captcha-Token'] = risk.captchaToken
config.headers['X-Device-Fp'] = getFingerprint()
}
return config
})
instance.interceptors.response.use(null, async (error) => {
// 服务端风控判定为可疑请求,要求二次验证
if (error.response?.status === 403 &&
error.response.data?.code === 'RISK_CHALLENGE') {
const { verify } = useCaptcha()
await verify() // 弹出验证,等待用户完成
return instance(error.config) // 用新 token 重放原请求
}
return Promise.reject(error)
})这里有一个重要的安全原则:token 必须一次性使用且由服务端验证,前端只做传输。绝对不要在前端判断验证是否通过就放行业务逻辑,那等于把门禁钥匙挂在门外。服务端校验 token 时应同时校验它与当前会话、当前接口的绑定关系,防止 token 被拿到别的接口重放。
四、整体时序与常见坑
把上面的模块串起来,完整时序是:页面加载时初始化采集器与验证 SDK,用户操作过程中持续累积行为信号,敏感操作触发验证获取 token,请求发出时拦截器注入 token 与指纹,服务端风控引擎综合 token 校验结果与行为信号打分,低分放行、中分挑战、高分拦截。
实际落地中有几个坑值得提醒。其一,验证码 SDK 尽量延迟加载,在用户即将触达敏感操作时再初始化,可以显著降低被自动化工具提前探测的风险。其二,指纹计算不要阻塞主线程,哈希运算量大时放到 Web Worker 里。其三,反欺诈规则要保留人工申诉通道,风控误伤真实用户的代价往往比漏掉几个机器人更高。其四,前端所有信号都只是参考,服务端必须独立校验,假设前端代码完全被逆向也不应导致防线崩溃。
工程化的意义就在于:验证能力像水电一样埋进基础设施里,业务开发只管调用,安全团队在统一的层里持续演进对抗策略。这套架构在 Vue 3 加 Pinia 加 Axios 的组合上跑通后,后续接入新的风控信号源、更换验证厂商,都只是局部调整,不会牵动整个应用。