Go + Vue 接入行为验证码完整指南

前言

在现代 Web 应用中,验证码是防止机器人攻击和恶意请求的重要手段。相比传统的图形验证码,滑动行为验证码具有更好的用户体验。本文将介绍如何使用 go-captcha 库在 Go 后端和 Vue 前端实现滑动验证码功能。

技术栈

  • 后端:Go + Gin 框架
  • 前端:Vue 3 + Element Plus
  • 验证码库:go-captcha(后端)+ go-captcha-vue(前端)
  • 缓存:Redis(用于存储验证码数据)

一、后端实现

1.1 安装依赖

bash 复制代码
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 文件:

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 文件:

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 文件:

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 文件:

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 中注册路由:

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 安装依赖

bash 复制代码
npm install go-captcha-vue
npm install element-plus

2.2 创建验证码组件

创建 components/SlideCaptcha.vue 文件:

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 使用验证码组件

在需要使用验证码的页面中:

vue 复制代码
<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 文件:

javascript 复制代码
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 验证码生成流程

  1. 前端点击"获取验证码"按钮
  2. 前端调用 /api/captcha 接口
  3. 后端生成验证码图片和答案坐标
  4. 后端将完整数据(包含答案)存储到 Redis,有效期1分钟
  5. 后端返回验证码ID和图片数据(不包含答案)给前端
  6. 前端展示滑动验证码弹窗

3.2 验证码验证流程

  1. 用户拖动滑块到目标位置
  2. 前端获取滑块坐标,调用 /api/captcha/verify 接口
  3. 后端从 Redis 获取验证码答案数据
  4. 后端比对用户滑动坐标与答案坐标(允许一定偏差)
  5. 验证成功后删除 Redis 数据,返回成功响应
  6. 前端收到成功响应后执行后续业务逻辑

3.3 安全性说明

  1. 答案不暴露:验证码答案坐标只存储在服务端 Redis 中,不返回给前端
  2. 一次性使用:验证成功后立即删除 Redis 数据,防止重复使用
  3. 时效性:验证码有效期1分钟,过期自动失效
  4. 偏差容忍:允许用户滑动有一定误差(默认10像素),提升用户体验

四、常见问题

4.1 验证码图片不显示

检查 base64 编码是否正确,确保前端正确解析 data:image/png;base64, 前缀。

4.2 验证总是失败

检查偏差值设置是否合理,可以适当增大 Deviation 常量的值。

4.3 Redis 连接失败

确保 Redis 服务已启动,检查连接地址和端口是否正确。

4.4 跨域问题

在 Gin 中添加 CORS 中间件:

go 复制代码
import "github.com/gin-contrib/cors"

r.Use(cors.Default())

五、总结

本文介绍了如何使用 go-captcha 库在 Go + Vue 项目中实现滑动验证码功能。核心要点:

  1. 后端使用 go-captcha 生成验证码图片和答案
  2. 使用 Redis 存储验证码数据,保证安全性和时效性
  3. 前端使用 go-captcha-vue 组件展示验证码
  4. 验证流程简单清晰,用户体验良好

完整代码可以直接应用到新项目中,只需根据实际情况调整路由、响应格式等细节即可。

参考资源

相关推荐
用户342323237631710 分钟前
开源!Go+Wails+Vue3 手搓一个 PLC 实时监控桌面工具
go
止语Lab1 小时前
为什么你的 Go TCP server P99 延迟这么高
go
Andy Dennis7 小时前
nsq学习记录
消息队列·go·nsq
韦胖漫谈IT9 小时前
选语言不是站队,是选适合问题的工具
java·python·ai·rust·go·技术落地
喵个咪20 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
夜悊1 天前
Go网络编程的学习代码示例:客户端/服务端(C/S)模型
go
审判长烧鸡1 天前
【AI问答】GO代码循环返值
go
捧 花1 天前
Eino框架记忆功能实现指南
go·agent·eino
Java陈序员1 天前
主流数据库通吃!一款开源实用的数据库备份管理工具!
react.js·postgresql·go
云浪1 天前
搞懂 Go WaitGroup:一篇文章彻底理解并发等待机制
后端·go