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 验证码生成流程

  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 中间件:

import "github.com/gin-contrib/cors"

r.Use(cors.Default())

五、总结

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

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

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

参考资源

posted @ 2026-03-31 21:00  牛奔  阅读(2)  评论(0)    收藏  举报