前言

在现代 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 文件:






2.3 使用验证码组件

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




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. 验证流程简单清晰,用户体验良好

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

参考资源


原文地址: https://www.cveoy.top/t/topic/qGjo 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录