Vue项目接口防刷加固:接入腾讯云天御验证码实现人机验证、恶意请求拦截

本文最后更新于 2025年9月12日 下午

我们在设计公共业务接口的时候(e.g. 登录、公共 API 请求等),为了防止恶意请求,我们通常会采用人机验证、恶意请求拦截等手段来保护我们的接口。本文将在 Vue3 项目基础上介绍如何使用腾讯云天御验证码来实现人机验证和恶意请求拦截。

人机验证

网站的数据通常都是列表、分页展示,存在一定的规律,比如 WordPress 的评论列表,每页显示 10 条,每次的翻页就是通过 API 接口提取数据库的数据进行展示。 如果没有人机验证或者恶意请求的拦截,那么攻击者就可以通过爬虫程序,模拟用户行为,不断发起请求,获取数据,从而造成数据泄露、服务器负载过高、带宽消耗过大等问题。

除了后台 API 限制请求频率这种“保守防御”,我们可以采用一些更“精巧”的方式。比如本文介绍的验证码,通过人机验证,可以有效地防止爬虫程序的恶意请求。

超级期待

人机验证码

验证码服务,其实形式很多。早些年登录 QQ 时候,弹出的“请输入图形中的数字/字母”就是一种验证码服务。原理就是通过随机生成一张图片,图片中包含一些数字或字母,让用户输入,如果输入正确,则允许登录,否则拒绝登录。

flowchart LR
    A[👤 用户访问] --> B[🎲 生成验证码]
    B --> C[📤 提交验证]
    C --> D{🔍 校验结果}
    D -->|✅ 成功| E[🎉 通过验证]
    D -->|❌ 失败| B
    
    style A fill:#e3f2fd,stroke:#1976d2
    style B fill:#fff3e0,stroke:#f57c00
    style E fill:#e8f5e8,stroke:#388e3c

输入图形数字这种形式已经很少了,随着样本数据和计算能力的提升,图形数字能拦截的基本只有“真人”。现在更流行的是滑动验证码、点选验证码、语音验证码等。比如: 我们这次介绍的腾讯云天御验证码。

滑动验证码

滑动验证码,顾名思义,就是需要用户拖动滑块,使滑块与缺口对齐,才能通过验证。这种验证码形式,判断用户能否把滑块对齐缺口只是验证的第一步;在验证的过程中,还会判定用户的滑动轨迹是否正常、Cookies是否异常等,只有全部通过,才会认为用户是真人,从而放行。

滑动验证码

同时,相比以前传统的后端传图验证码,滑动验证码通常前台验证后,生成票据;后端接口可以校验票据是否有效,从而减少后端压力。类似于 JWT 这种模式。

天御验证码

我们这次就以天御验证码为例,介绍如何接入人机验证和恶意请求拦截。腾讯云天御验证码的官网地址是:

使用的场景一般在网站、APP、小程序等场景,看到官方有 React 的接入指南,但是没有 Vue,其实原理差不多,这次我们自己封转一个 Vue 组。

原理是前端请求验证码的接口,用户完成验证后,返回票据;之后前端携带票据到后端验证票据是否有效:

sequenceDiagram
    participant U as 👤 用户
    participant F as 🌐 前端
    participant C as 🛡️ 验证码服务
    participant B as 🔐 后端
    
    U->>F: 访问页面
    F->>C: 请求验证码
    C->>F: 返回滑块验证
    U->>F: 完成验证
    F->>C: 提交结果
    
    alt 验证成功
        C->>F: 返回票据 🎫
        F->>B: 携带票据请求
        B->>C: 校验票据
        C->>B: 验证结果
        B->>F: 返回数据 ✅
    else 验证失败
        C->>F: 验证失败 ❌
        F->>U: 重新验证
    end

使用体验

在教程正式开始之际,我们来看一下最后的接入效果 🤔?

哈哈,有点小期待

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

薄荷文档 RAG 功能的验证码确认

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

网络控制台查看发送的请求体

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

腾讯云控制台调整验证码模式

操作前提

基础的网站开发知识,以及 Vue 的基础使用,这里不做赘述。完整的验证码业务,前端请求腾讯云验证码的接口来获取票据,后端接收票据,校验票据的有效性。

本次的操作,我前端就是使用 Vue3,后端就是用的 Golang。

当然,我们需要先开通腾讯云天御验证码的服务,新用户有 2w 次的免费额度,足够我们测试了:

天御验证码的免费额度

Vue 前端接入

前端的接入,我们可以参考官方的 Web 客户端接入React 版本接入Demo 来完成 Vue 的实现。

首先是定义容器,我们创建一个 component 组件,用来承载验证码的组件,并创建一个容器,用来后续的验证码挂载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<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事件,传递票据和随机字符串到上级父组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 成功回调
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 事件,把票据和随机字符串传递给后端,从而决定是否响应这次请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<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 前端接入的完整流程:

flowchart LR
    A[🚀 开通服务获取AppId] --> B[⚡ Vue组件]
    
    B --> C{📱 启用验证?}
    C -->|否| D[⏭️ 跳过验证]
    C -->|是| E[🎯 加载验证码]
    
    E --> F{👆 用户操作}
    F -->|✅ 成功| G[🎫 传递票据到后端]
    F -->|❌ 失败| E
    
    G --> H[✨ 后端校验]    
    D --> H
    
    %% 样式定义
    style A fill:#4fc3f7,stroke:#0288d1,stroke-width:3px,color:#fff,font-weight:bold
    style B fill:#81c784,stroke:#388e3c,stroke-width:2px,color:#fff,font-weight:bold
    style C fill:#ffb74d,stroke:#f57c00,stroke-width:2px,color:#fff,font-weight:bold
    style D fill:#a1887f,stroke:#5d4037,stroke-width:2px,color:#fff
    style E fill:#f06292,stroke:#c2185b,stroke-width:2px,color:#fff,font-weight:bold
    style F fill:#ba68c8,stroke:#7b1fa2,stroke-width:2px,color:#fff,font-weight:bold
    style G fill:#4db6ac,stroke:#00695c,stroke-width:2px,color:#fff,font-weight:bold
    style H fill:#ff8a65,stroke:#d84315,stroke-width:2px,color:#fff,font-weight:bold
    
    %% 连接线样式
    linkStyle 0 stroke:#0288d1,stroke-width:3px
    linkStyle 1 stroke:#388e3c,stroke-width:2px
    linkStyle 2 stroke:#f57c00,stroke-width:2px
    linkStyle 3 stroke:#c2185b,stroke-width:2px
    linkStyle 4 stroke:#7b1fa2,stroke-width:2px
    linkStyle 5 stroke:#00695c,stroke-width:2px
    linkStyle 6 stroke:#c2185b,stroke-width:2px,stroke-dasharray: 5 5
    linkStyle 7 stroke:#2e7d32,stroke-width:3px
    linkStyle 8 stroke:#5d4037,stroke-width:2px,stroke-dasharray: 3 3

当然,只是解决了前端接入,还需要后端配合,才能真正实现验证功能。接下来就看看后端怎么校验票据有效性。

Go 后端校验

后端的接入,官方的文档直接指引我们到 API Explorer 进行在线调试和代码生成。

官方的参考文档

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

官方的代码生成

比如我们的适配,首先是定义一个结构体读取我们的腾讯云 SecretId、SecretKey 和 验证码 CaptchaAppId 等配置:

1
2
3
4
5
6
7
8
9
// 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"`
}

然后就是初始化时候读取配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 验证码配置
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
}
}

在请求体内添加ticketrandstr字段的映射(CaptchaTicket 和 CaptchaRandstr):

1
2
3
4
5
6
7
// 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"`
}

在控制层,也就是接口处理函数中,我们就可以通过CaptchaTicketCaptchaRandstr获取到票据和随机字符串,然后调用腾讯云的接口进行校验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// 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)
}

总体来说,还是很简单的。联动一下前端,整个流程就是:

sequenceDiagram
    participant U as 👤 用户
    participant V as 🟢 Vue前端
    participant T as 🛡️ 腾讯云服务
    participant G as 🔵 Go后端
    
    Note over U,G: 完整的验证码集成流程
    
    U->>V: 1. 访问页面/触发验证
    V->>V: 2. 检查是否启用验证码
    
    alt 启用验证码
        V->>T: 3. 加载验证码组件
        T->>V: 4. 返回滑块验证界面
        V->>U: 5. 展示验证码
        
        U->>T: 6. 完成滑块验证
        T->>V: 7. 返回票据(ticket+randstr)
        
        V->>G: 8. 发送请求(携带票据)
        Note right of V: 请求体包含:<br/>- Query: 用户输入<br/>- CaptchaTicket: 验证票据<br/>- CaptchaRandstr: 随机字符串
        
        G->>G: 9. 解析请求参数
        G->>T: 10. 调用DescribeCaptchaResult API
        Note right of G: 验证参数:<br/>- CaptchaAppId<br/>- Ticket<br/>- Randstr<br/>- 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
    

完整的代码可以参考:

未来期待

其实,我个人也有用过其他的验证码,比如: Geetest(极验)Google reCAPTCHA

个人觉得腾讯云天御验证码的接入还是比较方便的,尤其是 API Explorer 的在线调试和代码生成,还是非常方便的。

但是在形式上,还是有些欠缺…… 没有看到 Geetest 那样的九宫格点选、五子棋验证等创意的交互形式。缺少了一些创新。在计费上,单次价格 0.005元/次,还是比较贵的(对于个人开发者而已🤔)。

END

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

最后,如果你觉得本篇教程对你有帮助,欢迎加入我们的开发者交流群: 812198734 ,一起交流学习,共同进步。



Vue项目接口防刷加固:接入腾讯云天御验证码实现人机验证、恶意请求拦截
https://www.mintimate.cn/2025/09/12/qcloudCaptchaIntegration/
作者
Mintimate
发布于
2025年9月12日
许可协议