我们在设计公共业务接口的时候(e.g. 登录、公共 API 请求等),为了防止恶意请求,我们通常会采用人机验证、恶意请求拦截等手段来保护我们的接口。本文将在 Vue3 项目基础上介绍如何使用腾讯云天御验证码来实现人机验证和恶意请求拦截。
人机验证
网站的数据通常都是列表、分页展示,存在一定的规律,比如 WordPress 的评论列表,每页显示 10 条,每次的翻页就是通过 API 接口提取数据库的数据进行展示。 如果没有人机验证或者恶意请求的拦截,那么攻击者就可以通过爬虫程序,模拟用户行为,不断发起请求,获取数据,从而造成数据泄露、服务器负载过高、带宽消耗过大等问题。
除了后台 API 限制请求频率这种"保守防御",我们可以采用一些更"精巧"的方式。比如本文介绍的验证码,通过人机验证,可以有效地防止爬虫程序的恶意请求。

人机验证码
验证码服务,其实形式很多。早些年登录 QQ 时候,弹出的"请输入图形中的数字/字母"就是一种验证码服务。原理就是通过随机生成一张图片,图片中包含一些数字或字母,让用户输入,如果输入正确,则允许登录,否则拒绝登录。
输入图形数字这种形式已经很少了,随着样本数据和计算能力的提升,图形数字能拦截的基本只有"真人"。现在更流行的是滑动验证码、点选验证码、语音验证码等。比如: 我们这次介绍的腾讯云天御验证码。
滑动验证码
滑动验证码,顾名思义,就是需要用户拖动滑块,使滑块与缺口对齐,才能通过验证。这种验证码形式,判断用户能否把滑块对齐缺口只是验证的第一步;在验证的过程中,还会判定用户的滑动轨迹是否正常、Cookies是否异常等,只有全部通过,才会认为用户是真人,从而放行。

同时,相比以前传统的后端传图验证码,滑动验证码通常前台验证后,生成票据;后端接口可以校验票据是否有效,从而减少后端压力 。类似于 JWT 这种模式。
视频演示
为了让大家更直观地感受各家验证码在适配后的样式效果,录制了一个 1.5min 的演示视频。
做教程视频不易(技术教程,受众小,B站根本不会推荐引流,甚至经常把我回复的技术评论屏蔽),请务必一键三连嗷~
- Bilibili 视频地址:www.bilibili.com/video/BV1MM...
- Youtube 视频地址:www.youtube.com/shorts/r7bg...
天御验证码
我们这次就以天御验证码为例,介绍如何接入人机验证和恶意请求拦截。腾讯云天御验证码的官网地址是:
使用的场景一般在网站、APP、小程序等场景,看到官方有 React 的接入指南,但是没有 Vue,其实原理差不多,这次我们自己封转一个 Vue 组。
原理是前端请求验证码的接口,用户完成验证后,返回票据;之后前端携带票据到后端验证票据是否有效:
使用体验
在教程正式开始之际,我们来看一下最后的接入效果 🤔?

可以在 薄荷文档 上的 AI 功能进行提问,会自动触发验证码,如下图所示:

我们通过验证后,把前端会把生成的票据作为参数传递给后端,后端验证票据的有效性决定是否放行:

当然,你也可以在腾讯云的控制台上,切换验证码的样式、风控等级等:

其实我也是有接入过极验验证码、Google reCAPTCHA的,对比之下主要的区别:
对比项目 | 腾讯云天御 | 极验 | Google reCAPTCHA |
---|---|---|---|
验证方式 | 滑动拼图、文字点选、图形选择、VVT空间语义、无感验证 | 仅滑动拼图 | 复合验证(V2)、无感验证(V3) |
价格 | 收费 | 低频使用不收费 | 免费(用量限制: 1万/月) |
破解难度 | 较高,破解方法和教程较少,特别是VVT空间语义 | 较低,网上破解方法和教程较多 | 中等,基于机器学习算法 |
支持平台 | Web/H5、iOS、Android、小程序、鸿蒙 | Web/H5、iOS、Android、小程序、鸿蒙 | Web/H5、iOS、Android |
验证流程 | 前端生成票据,后端密钥验证 | 前端生成票据,后端密钥验证 | 前端生成票据,后端密钥验证(注意: Google Cloud 版本的密钥和 reCAPTCHA 版本验证密钥不一样) |
安全校验 | 支持客户端和服务端 CaptchaAppId 强制校验 | 不支持强制校验 | 支持域名白名单 |
个性化展示 | 弹出式(popup)、内嵌式(embed) | float、popup、bind 三种模式 | 无。不过无感验证体验很好 |

关于极验的定价,咨询了商务。理论上都需要收费,并且最低为1万/年。但是根据论坛小伙伴的说法,个人开发者使用,月度验证次数低的情况下,可以一直使用。
操作前提
基础的网站开发知识,以及 Vue 的基础使用,这里不做赘述。完整的验证码业务,前端请求腾讯云验证码的接口来获取票据,后端接收票据,校验票据的有效性。
本次的操作,我前端就是使用 Vue3,后端就是用的 Golang。
当然,我们需要先开通腾讯云天御验证码的服务,新用户有 2w 次的免费额度,足够我们测试了:

Vue 前端接入
前端的接入,我们可以参考官方的 Web 客户端接入和 React 版本接入Demo 来完成 Vue 的实现。
首先是定义容器,我们创建一个 component 组件,用来承载验证码的组件,并创建一个容器,用来后续的验证码挂载:
js
<template>
<div v-if="isVisible" class="captcha-status">
<div class="captcha-indicator">
<svg class="captcha-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1M12,7C13.4,7 14.8,8.6 14.8,10V11H16V18H8V11H9.2V10C9.2,8.6 10.6,7 12,7M12,8.2C11.2,8.2 10.4,8.7 10.4,10V11H13.6V10C13.6,8.7 12.8,8.2 12,8.2Z" />
</svg>
<span>{{ statusText }}</span>
</div>
<div :id="containerId" class="captcha-container"></div>
</div>
</template>
<script setup>
import { ref, nextTick, onUnmounted, watch } from 'vue'
/**
* 腾讯云天御验证码组件
* 作者: Mintimate
* 创建时间: 2025-09-11
* 描述: 可复用的腾讯云验证码组件,支持嵌入式验证码
*/
// Props
const props = defineProps({
appId: {
type: String,
required: true,
default: '1234567890'
},
enabled: {
type: Boolean,
default: true
},
show: {
type: Boolean,
default: false
},
statusText: {
type: String,
default: '请完成安全验证...'
},
containerId: {
type: String,
default: 'captcha-embed'
},
embedMode: {
type: Boolean,
default: false
}
})
// Emits
const emit = defineEmits(['success', 'cancel', 'error', 'show', 'hide'])
// 响应式状态
const isVisible = ref(false)
const captchaInstance = ref(null)
const captchaTicket = ref('') // 验证成功后返回的票据
const captchaRandstr = ref('') // 验证成功后返回的随机字符串
<!-- 其他代码... -->
</script>
添加的 props
接受父组件参数。其中:
appId
是天御验证码的 CaptchaAppId;enabled
表示是否启用验证码,默认为true
;show
表示是否显示验证码,默认为false
;statusText
表示验证码状态文案,默认为请完成安全验证...
;containerId
验证码容器的 ID,默认为captcha-embed
;为我们使用验证码的嵌入模式时候,进行替换展示的容器 ID(天御验证码有两种模式,一种是嵌入模式,一种是弹窗模式)。
之后的验证,关键代码自然是触发emit
内的success
事件,传递票据和随机字符串到上级父组件。
js
// 成功回调
const successCallback = () => {
// 检查验证码容器是否存在
const captchaContainer = document.getElementById(props.containerId)
if (!captchaContainer) {
console.error('验证码容器不存在')
captchaLoadErrorCallback()
return
}
// 清理容器内容
captchaContainer.innerHTML = ''
// 动态加载验证码脚本
if (typeof window.TencentCaptcha === 'undefined') {
try {
// 腾讯云天御验证码前端 JS: https://turing.captcha.qcloud.com/TJCaptcha.js
await import('./resources/captcha/TCaptcha.js')
} catch (importError) {
console.error('验证码脚本加载失败:', importError)
captchaLoadErrorCallback()
return
}
}
// 检查 TencentCaptcha 是否可用
if (typeof window.TencentCaptcha === 'undefined') {
console.error('TencentCaptcha 未加载')
captchaLoadErrorCallback()
return
}
// 等待 100ms,确保 TencentCaptcha 已经加载完成
await new Promise(resolve => setTimeout(resolve, 100))
// 创建验证码实例
captchaInstance.value = new window.TencentCaptcha(captchaContainer, props.appId, captchaCallback, {
type: props.embedMode ? 'embed' : 'popup'
})
// 显示验证码
captchaInstance.value.show()
}
我们在父组件中监听 success
事件,把票据和随机字符串传递给后端,从而决定是否响应这次请求:
js
<template>
<!-- 验证码组件 -->
<qCloudCaptcha
:app-id="captchaAppId"
:enabled="enableCaptcha"
:show="captchaState.isVerifying"
embed-mode
@success="onCaptchaSuccess"
@cancel="onCaptchaCancel"
@error="onCaptchaError"
@hide="onCaptchaHide"
/>
</template>
<script setup>
import qCloudCaptcha from './qCloudCaptcha.vue'
// 验证码组件事件处理
const onCaptchaSuccess = (data) => {
captchaState.value.ticket = data.ticket
captchaState.value.randstr = data.randstr
captchaState.value.isVerifying = false
// 验证成功后继续调用接口消息(并使用captchaState和randstr作为验证码票据传给接口)
proceedWithMessage()
}
// 其他代码...
</script>
完整的代码可以参考:
我们总结一下 Vue 前端接入的完整流程:
当然,只是解决了前端接入,还需要后端配合,才能真正实现验证功能。接下来就看看后端怎么校验票据有效性。
Go 后端校验
后端的接入,官方的文档直接指引我们到 API Explorer 进行在线调试和代码生成。

在调用成功以后,有一个基础的代码生成供我们进行自己的业务改造:

比如我们的适配,首先是定义一个结构体读取我们的腾讯云 SecretId、SecretKey 和 验证码 CaptchaAppId 等配置:
go
// CaptchaConfig 验证码配置
type CaptchaConfig struct {
SecretID string `yaml:"secret_id"`
SecretKey string `yaml:"secret_key"`
CaptchaAppID uint64 `yaml:"captcha_app_id"`
AppSecretKey string `yaml:"app_secret_key"`
Endpoint string `yaml:"endpoint"`
CaptchaType uint64 `yaml:"captcha_type"`
}
然后就是初始化时候读取配置文件:
go
// 验证码配置
if secretID := os.Getenv("TENCENTCLOUD_SECRET_ID"); secretID != "" {
config.Captcha.SecretID = secretID
}
if secretKey := os.Getenv("TENCENTCLOUD_SECRET_KEY"); secretKey != "" {
config.Captcha.SecretKey = secretKey
}
if captchaAppID := os.Getenv("CAPTCHA_APP_ID"); captchaAppID != "" {
if appID, err := strconv.ParseUint(captchaAppID, 10, 64); err == nil {
config.Captcha.CaptchaAppID = appID
}
}
if appSecretKey := os.Getenv("CAPTCHA_APP_SECRET_KEY"); appSecretKey != "" {
config.Captcha.AppSecretKey = appSecretKey
}
if endpoint := os.Getenv("CAPTCHA_ENDPOINT"); endpoint != "" {
config.Captcha.Endpoint = endpoint
}
if captchaType := os.Getenv("CAPTCHA_TYPE"); captchaType != "" {
if cType, err := strconv.ParseUint(captchaType, 10, 64); err == nil {
config.Captcha.CaptchaType = cType
}
}
在请求体内添加ticket
和randstr
字段的映射(CaptchaTicket 和 CaptchaRandstr):
go
// ChatRequest 聊天请求结构
type ChatRequest struct {
Query string `json:"Query" binding:"required"`
History []ChatMessage `json:"History,omitempty"`
CaptchaTicket string `json:"CaptchaTicket,omitempty"`
CaptchaRandstr string `json:"CaptchaRandstr,omitempty"`
}
在控制层,也就是接口处理函数中,我们就可以通过CaptchaTicket
和CaptchaRandstr
获取到票据和随机字符串,然后调用腾讯云的接口进行校验:
go
// NewCaptchaService 创建验证码服务实例
func NewCaptchaService(cfg *config.CaptchaConfig) (*CaptchaService, error) {
if cfg.SecretID == "" || cfg.SecretKey == "" {
return nil, fmt.Errorf("验证码服务配置不完整:缺少 SecretID 或 SecretKey")
}
// 实例化认证对象
credential := common.NewCredential(cfg.SecretID, cfg.SecretKey)
// 实例化客户端配置对象
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = cfg.Endpoint
// 实例化要请求产品的client对象
client, err := captcha.NewClient(credential, "", cpf)
if err != nil {
return nil, fmt.Errorf("创建验证码客户端失败: %v", err)
}
return &CaptchaService{
client: client,
config: cfg,
}, nil
}
// VerifyCaptcha 验证验证码
func (s *CaptchaService) VerifyCaptcha(ticket, randstr, userIP string) (bool, error) {
if ticket == "" || randstr == "" {
return false, fmt.Errorf("验证码参数不能为空")
}
// 如果没有提供用户IP,尝试获取本地IP
if userIP == "" {
userIP = s.getLocalIP()
}
// 实例化请求对象
request := captcha.NewDescribeCaptchaResultRequest()
request.CaptchaType = common.Uint64Ptr(s.config.CaptchaType)
request.Ticket = common.StringPtr(ticket)
request.UserIp = common.StringPtr(userIP)
request.Randstr = common.StringPtr(randstr)
request.CaptchaAppId = common.Uint64Ptr(s.config.CaptchaAppID)
request.AppSecretKey = common.StringPtr(s.config.AppSecretKey)
// 发送请求
response, err := s.client.DescribeCaptchaResult(request)
if err != nil {
if sdkErr, ok := err.(*errors.TencentCloudSDKError); ok {
logger.Error("验证码验证API错误: Code=%s, Message=%s", sdkErr.Code, sdkErr.Message)
return false, fmt.Errorf("验证码验证失败: %s", sdkErr.Message)
}
logger.Error("验证码验证请求失败: %v", err)
return false, fmt.Errorf("验证码验证请求失败: %v", err)
}
// 检查验证结果
if response.Response.CaptchaCode == nil {
return false, fmt.Errorf("验证码响应格式错误")
}
captchaCode := *response.Response.CaptchaCode
logger.Info("验证码验证结果: Code=%d", captchaCode)
// 验证码验证成功的状态码是1
if captchaCode == 1 {
return true, nil
}
// 根据不同的错误码返回相应的错误信息
var errorMsg string
switch captchaCode {
case 6:
errorMsg = "验证码已过期"
case 7:
errorMsg = "验证码已使用"
case 8:
errorMsg = "验证码验证失败"
case 9:
errorMsg = "验证码参数错误"
case 10:
errorMsg = "验证码配置错误"
case 100:
errorMsg = "验证码AppID不存在"
default:
errorMsg = fmt.Sprintf("验证码验证失败,错误码: %d", captchaCode)
}
return false, fmt.Errorf(errorMsg)
}
总体来说,还是很简单的。联动一下前端,整个流程就是:
- Query: 用户输入
- CaptchaTicket: 验证票据
- CaptchaRandstr: 随机字符串 G->>G: 9. 解析请求参数 G->>T: 10. 调用DescribeCaptchaResult API Note right of G: 验证参数:
- CaptchaAppId
- Ticket
- Randstr
- UserIP T->>G: 11. 返回验证结果 alt 验证成功(Code=1) G->>V: 12. 处理业务逻辑并返回结果 ✅ V->>U: 13. 展示成功结果 else 验证失败 G->>V: 14. 返回验证失败错误 ❌ V->>U: 15. 提示重新验证 end else 跳过验证码 V->>G: 直接发送请求 G->>V: 处理业务逻辑 V->>U: 返回结果 end
完整的代码可以参考:
未来期待
综合适配体验下来,天御验证码的集成难度相对较低。不过相比极验验证码,天御的适配难度仍然偏高,主要原因如下:
- 官方 Demo 局限性: 官方仅提供 React Demo,虽然在大模型时代框架转换不是难题,但如果能提供 Vue、Angular 等多框架 Demo 会显著提升开发体验
- 社区支持不足: 天御的社区教程,远远没有极验多,甚至没有国内不可用的 Google reCAPTCHA 社区教程多;这不仅体现在接入上,更多的是美化、故障排查只能依靠个人理解和官方支持。当然,这也是一把"双刃剑",社区上对于极验的破解教程也是非常多。但是验证码本身就是一种限频手段,被 破解反而不是很重要的。
甚至我们 TDP 开发者群的小伙伴,花了一个下午,天御验证码的破解 SDK 就做出来了......

因此,验证码被破解并非关键问题,更重要的是提升开发者体验。期待未来社区教程资源的完善和文档内容的丰富。
从实际使用体验来看,腾讯云的产品和服务表现优秀,这里也对未来发展提出一些建议:
验证码样式创新
- 当前控制台只能设置单一验证码类型,建议支持多种验证方式随机出现,这样能显著提升人机验证的效果
- 可以参考 Google reCAPTCHA 的智能弹出样式,根据用户行为动态选择验证方式
自定义验证码风格
- 目前滑动验证码只能使用系统预设样式,缺乏个性化选择多种并存的方式。
- 建议增加验证码图片主题设置(如海洋风格、都市景观、自然风光等),提升用户体验
- 在审核机制完善的前提下,进一步支持用户上传自定义背景图片,满足品牌定制需求

既然说到验证码本身是一种限评论手段,不妨试试看 Google reCAPTCHA 的评分机制:

END
好啦,今天就到这里了,感谢你的阅读,欢迎交流。