Go + Vue 接入行为验证码完整指南
前言
在现代 Web 应用中,验证码是防止机器人攻击和恶意请求的重要手段。相比传统的图形验证码,滑动行为验证码具有更好的用户体验。本文将介绍如何使用 go-captcha 库在 Go 后端和 Vue 前端实现滑动验证码功能。
技术栈
- 后端:Go + Gin 框架
- 前端:Vue 3 + Element Plus
- 验证码库:go-captcha(后端)+ go-captcha-vue(前端)
- 缓存:Redis(用于存储验证码数据)
一、后端实现
1.1 安装依赖
go get github.com/wenlng/go-captcha/v2
go get github.com/wenlng/go-captcha-assets
go get github.com/gin-gonic/gin
go get github.com/redis/go-redis/v9
1.2 初始化验证码模块
创建 captcha/init.go 文件:
package captcha
import (
"errors"
)
var (
ErrGenData = errors.New("generate data error")
)
const (
Deviation = 10 // 验证偏差值,允许用户滑动有一定误差
)
func Init() error {
return initSlide()
}
1.3 实现滑动验证码核心逻辑
创建 captcha/slide.go 文件:
package captcha
import (
images "github.com/wenlng/go-captcha-assets/resources/imagesv2"
"github.com/wenlng/go-captcha-assets/resources/tiles"
"github.com/wenlng/go-captcha/v2/base/option"
"github.com/wenlng/go-captcha/v2/slide"
)
var slideCapt slide.Captcha
// initSlide 初始化滑动验证码
func initSlide() error {
builder := slide.NewBuilder(
slide.WithGenGraphNumber(1),
slide.WithEnableGraphVerticalRandom(true),
slide.WithImageSize(option.Size{Width: 300, Height: 220}),
)
// 加载背景图片资源
imgs, err := images.GetImages()
if err != nil {
return err
}
// 加载滑块图形资源
graphs, err := tiles.GetTiles()
if err != nil {
return err
}
var newGraphs = make([]*slide.GraphImage, 0, len(graphs))
for i := range graphs {
graph := graphs[i]
newGraphs = append(newGraphs, &slide.GraphImage{
OverlayImage: graph.OverlayImage,
MaskImage: graph.MaskImage,
ShadowImage: graph.ShadowImage,
})
}
// 设置资源
builder.SetResources(
slide.WithGraphImages(newGraphs),
slide.WithBackgrounds(imgs),
)
slideCapt = builder.Make()
return nil
}
// Slide 验证码数据结构
type Slide struct {
// 滑块初始显示坐标
SliderX int
SliderY int
SliderWidth int
SliderHeight int
// 整体大小
MainWidth int
MainHeight int
// 答案坐标(服务端保存,不返回给前端)
X int
Y int
// 主图与滑块的图片,base64编码
MainImage string
SliderImage string
}
// NewSlide 生成新的滑动验证码
func NewSlide() (*Slide, error) {
m := &Slide{}
captData, err := slideCapt.Generate()
if err != nil {
return nil, err
}
dotData := captData.GetData()
if dotData == nil {
return nil, ErrGenData
}
// 答案坐标(正确的滑块位置)
m.X = dotData.X
m.Y = dotData.Y
// 滑块初始显示坐标
m.SliderWidth = dotData.Width
m.SliderHeight = dotData.Height
m.SliderX = dotData.DX
m.SliderY = dotData.DY
// 图片大小
m.MainWidth = 300
m.MainHeight = 220
// 转换为 base64 编码
m.MainImage, err = captData.GetMasterImage().ToBase64()
if err != nil {
return nil, err
}
m.SliderImage, err = captData.GetTileImage().ToBase64()
if err != nil {
return nil, err
}
return m, nil
}
// VerifySlide 验证滑动位置是否正确
func VerifySlide(userX, userY, slideX, slideY int) bool {
return slide.Validate(userX, userY, slideX, slideY, Deviation)
}
1.4 定义响应结构体
创建 vo/captcha.go 文件:
package vo
type GetCaptchaRes struct {
ID string `json:"id"` // 验证码唯一标识
SliderX int `json:"sliderX"` // 滑块初始X坐标
SliderY int `json:"sliderY"` // 滑块初始Y坐标
SliderWidth int `json:"sliderWidth"` // 滑块宽度
SliderHeight int `json:"sliderHeight"` // 滑块高度
SliderImage string `json:"sliderImage"` // 滑块图片(base64)
MainWidth int `json:"mainWidth"` // 主图宽度
MainHeight int `json:"mainHeight"` // 主图高度
MainImage string `json:"mainImage"` // 主图(base64)
}
1.5 实现 HTTP 处理器
创建 handler/captcha_handler.go 文件:
package handler
import (
"encoding/json"
"time"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
"github.com/rs/xid"
"your-project/captcha"
"your-project/vo"
)
type CaptchaHandler struct {
redis *redis.Client
}
func NewCaptchaHandler(redis *redis.Client) *CaptchaHandler {
return &CaptchaHandler{
redis: redis,
}
}
// GetCaptcha 获取验证码
func (h *CaptchaHandler) GetCaptcha(c *gin.Context) {
// 生成验证码
m, err := captcha.NewSlide()
if err != nil {
c.JSON(500, gin.H{"error": "Failed to create captcha"})
return
}
// 序列化验证码数据
value, err := json.Marshal(m)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to marshal captcha"})
return
}
// 生成唯一ID并存储到 Redis,有效期1分钟
uuid := xid.New().String()
err = h.redis.Set(c, uuid, value, time.Minute).Err()
if err != nil {
c.JSON(500, gin.H{"error": "Failed to store captcha"})
return
}
// 返回给前端的数据(不包含答案坐标)
res := &vo.GetCaptchaRes{
ID: uuid,
SliderX: m.SliderX,
SliderY: m.SliderY,
SliderWidth: m.SliderWidth,
SliderHeight: m.SliderHeight,
SliderImage: m.SliderImage,
MainWidth: m.MainWidth,
MainHeight: m.MainHeight,
MainImage: m.MainImage,
}
c.JSON(200, gin.H{"code": 0, "data": res})
}
// VerifyCaptcha 验证滑动验证码
func (h *CaptchaHandler) VerifyCaptcha(c *gin.Context) {
var data struct {
ID string `json:"id"` // 验证码标识
X int `json:"x"` // 用户滑动的X坐标
Y int `json:"y"` // 用户滑动的Y坐标
}
if err := c.ShouldBindJSON(&data); err != nil {
c.JSON(400, gin.H{"error": "Invalid arguments"})
return
}
// 从 Redis 获取验证码数据
value, err := h.redis.Get(c, data.ID).Result()
if err != nil {
c.JSON(400, gin.H{"error": "验证码已过期或不存在"})
return
}
// 反序列化验证码数据
slide := captcha.Slide{}
err = json.Unmarshal([]byte(value), &slide)
if err != nil {
c.JSON(500, gin.H{"error": "验证码数据错误"})
return
}
// 验证滑动位置
if !captcha.VerifySlide(data.X, data.Y, slide.X, slide.Y) {
c.JSON(400, gin.H{"error": "验证码验证失败"})
return
}
// 验证成功后删除 Redis 中的数据(防止重复使用)
h.redis.Del(c, data.ID)
c.JSON(200, gin.H{"code": 0, "message": "验证成功"})
}
1.6 注册路由
在 main.go 中注册路由:
package main
import (
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
"your-project/captcha"
"your-project/handler"
)
func main() {
// 初始化验证码模块
if err := captcha.Init(); err != nil {
panic(err)
}
// 初始化 Redis 客户端
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// 创建 Gin 路由
r := gin.Default()
// 创建处理器
captchaHandler := handler.NewCaptchaHandler(rdb)
// 注册路由
api := r.Group("/api")
{
api.POST("/captcha", captchaHandler.GetCaptcha)
api.POST("/captcha/verify", captchaHandler.VerifyCaptcha)
}
r.Run(":8080")
}
二、前端实现
2.1 安装依赖
npm install go-captcha-vue
npm install element-plus
2.2 创建验证码组件
创建 components/SlideCaptcha.vue 文件:
<template>
<div class="slide-captcha-wrapper">
<el-button
:disabled="!canSend"
@click="handleClick"
class="trigger-btn"
>
{{ btnText }}
</el-button>
<!-- 滑动验证码弹窗 -->
<el-dialog
v-model="showDialog"
width="326px"
:close-on-click-modal="false"
:show-close="false"
:append-to-body="true"
>
<GoCaptchaSlide
v-if="captchaData"
:config="captchaConfig"
:data="captchaData"
:events="captchaEvents"
/>
</el-dialog>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { Slide as GoCaptchaSlide } from 'go-captcha-vue'
import { httpPost } from '@/utils/http'
// Props
const props = defineProps({
btnText: {
type: String,
default: '发送验证码'
}
})
// Emits
const emit = defineEmits(['success'])
// 状态
const showDialog = ref(false)
const captchaData = ref(null)
const captchaKey = ref('')
const canSend = ref(true)
const btnText = ref(props.btnText)
// 滑动验证码配置
const captchaConfig = {
width: 300,
height: 220,
thumbWidth: 60,
thumbHeight: 60,
showTheme: true,
title: '请拖动滑块完成验证'
}
// 滑动验证码事件
const captchaEvents = {
confirm: (point, reset) => {
verifyCaptcha({
x: Math.floor(point.x),
y: Math.floor(point.y)
})
return false
},
refresh: () => {
loadCaptcha()
},
close: () => {
showDialog.value = false
}
}
// 点击按钮触发
const handleClick = () => {
if (!canSend.value) return
loadCaptcha()
}
// 加载滑动验证码
const loadCaptcha = () => {
httpPost('/api/captcha')
.then((res) => {
captchaKey.value = res.data.id
captchaData.value = {
image: res.data.mainImage,
thumb: res.data.sliderImage,
thumbX: res.data.sliderX,
thumbY: res.data.sliderY,
thumbWidth: res.data.sliderWidth,
thumbHeight: res.data.sliderHeight
}
showDialog.value = true
})
.catch((e) => {
ElMessage.error('获取验证码失败:' + e.message)
})
}
// 验证滑动验证码
const verifyCaptcha = (verifyData) => {
httpPost('/api/captcha/verify', {
id: captchaKey.value,
x: verifyData.x,
y: verifyData.y
})
.then(() => {
showDialog.value = false
ElMessage.success('验证成功')
emit('success')
})
.catch((e) => {
ElMessage.error('验证失败:' + e.message)
// 验证失败,重新加载验证码
captchaData.value = null
loadCaptcha()
})
}
// 暴露方法供父组件调用
defineExpose({
loadCaptcha
})
</script>
<style scoped>
.slide-captcha-wrapper {
display: inline-block;
}
.trigger-btn {
width: 100%;
}
</style>
2.3 使用验证码组件
在需要使用验证码的页面中:
<template>
<div class="login-form">
<el-form>
<el-form-item label="手机号">
<el-input v-model="mobile" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="验证码">
<el-input v-model="code" placeholder="请输入验证码">
<template #append>
<SlideCaptcha
@success="handleCaptchaSuccess"
btn-text="获取验证码"
/>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import SlideCaptcha from '@/components/SlideCaptcha.vue'
import { httpPost } from '@/utils/http'
const mobile = ref('')
const code = ref('')
// 验证码验证成功后的回调
const handleCaptchaSuccess = () => {
// 发送短信验证码
httpPost('/api/sms/send', { mobile: mobile.value })
.then(() => {
ElMessage.success('验证码已发送')
})
.catch((e) => {
ElMessage.error('发送失败:' + e.message)
})
}
const handleLogin = () => {
// 登录逻辑
console.log('登录', mobile.value, code.value)
}
</script>
2.4 HTTP 工具函数
创建 utils/http.js 文件:
import axios from 'axios'
const http = axios.create({
baseURL: 'http://localhost:8080',
timeout: 10000
})
// 请求拦截器
http.interceptors.request.use(
config => {
// 可以在这里添加 token
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
http.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 0) {
return Promise.reject(new Error(res.error || 'Error'))
}
return res
},
error => {
return Promise.reject(error)
}
)
export const httpPost = (url, data) => {
return http.post(url, data)
}
export const httpGet = (url, params) => {
return http.get(url, { params })
}
三、核心流程说明
3.1 验证码生成流程
- 前端点击"获取验证码"按钮
- 前端调用
/api/captcha接口 - 后端生成验证码图片和答案坐标
- 后端将完整数据(包含答案)存储到 Redis,有效期1分钟
- 后端返回验证码ID和图片数据(不包含答案)给前端
- 前端展示滑动验证码弹窗
3.2 验证码验证流程
- 用户拖动滑块到目标位置
- 前端获取滑块坐标,调用
/api/captcha/verify接口 - 后端从 Redis 获取验证码答案数据
- 后端比对用户滑动坐标与答案坐标(允许一定偏差)
- 验证成功后删除 Redis 数据,返回成功响应
- 前端收到成功响应后执行后续业务逻辑
3.3 安全性说明
- 答案不暴露:验证码答案坐标只存储在服务端 Redis 中,不返回给前端
- 一次性使用:验证成功后立即删除 Redis 数据,防止重复使用
- 时效性:验证码有效期1分钟,过期自动失效
- 偏差容忍:允许用户滑动有一定误差(默认10像素),提升用户体验
四、常见问题
4.1 验证码图片不显示
检查 base64 编码是否正确,确保前端正确解析 data:image/png;base64, 前缀。
4.2 验证总是失败
检查偏差值设置是否合理,可以适当增大 Deviation 常量的值。
4.3 Redis 连接失败
确保 Redis 服务已启动,检查连接地址和端口是否正确。
4.4 跨域问题
在 Gin 中添加 CORS 中间件:
import "github.com/gin-contrib/cors"
r.Use(cors.Default())
五、总结
本文介绍了如何使用 go-captcha 库在 Go + Vue 项目中实现滑动验证码功能。核心要点:
- 后端使用 go-captcha 生成验证码图片和答案
- 使用 Redis 存储验证码数据,保证安全性和时效性
- 前端使用 go-captcha-vue 组件展示验证码
- 验证流程简单清晰,用户体验良好
完整代码可以直接应用到新项目中,只需根据实际情况调整路由、响应格式等细节即可。

浙公网安备 33010602011771号