Go + Vue 實現(xiàn)行為驗證碼完整指南
前言
在現(xiàn)代 Web 應用中,驗證碼是防止機器人攻擊和惡意請求的重要手段。相比傳統(tǒng)的圖形驗證碼,滑動行為驗證碼具有更好的用戶體驗。本文將介紹如何使用 go-captcha 庫在 Go 后端和 Vue 前端實現(xiàn)滑動驗證碼功能。
技術棧
- 后端:Go + Gin 框架
- 前端:Vue 3 + Element Plus
- 驗證碼庫:go-captcha(后端)+ go-captcha-vue(前端)
- 緩存:Redis(用于存儲驗證碼數(shù)據(jù))
一、后端實現(xiàn)
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 初始化驗證碼模塊
創(chuàng)建 captcha/init.go 文件:
package captcha
import (
"errors"
)
var (
ErrGenData = errors.New("generate data error")
)
const (
Deviation = 10 // 驗證偏差值,允許用戶滑動有一定誤差
)
func Init() error {
return initSlide()
}1.3 實現(xiàn)滑動驗證碼核心邏輯
創(chuàng)建 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 驗證碼數(shù)據(jù)結構
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 定義響應結構體
創(chuàng)建 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 實現(xiàn) HTTP 處理器
創(chuàng)建 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
}
// 序列化驗證碼數(shù)據(jù)
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
}
// 返回給前端的數(shù)據(jù)(不包含答案坐標)
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 獲取驗證碼數(shù)據(jù)
value, err := h.redis.Get(c, data.ID).Result()
if err != nil {
c.JSON(400, gin.H{"error": "驗證碼已過期或不存在"})
return
}
// 反序列化驗證碼數(shù)據(jù)
slide := captcha.Slide{}
err = json.Unmarshal([]byte(value), &slide)
if err != nil {
c.JSON(500, gin.H{"error": "驗證碼數(shù)據(jù)錯誤"})
return
}
// 驗證滑動位置
if !captcha.VerifySlide(data.X, data.Y, slide.X, slide.Y) {
c.JSON(400, gin.H{"error": "驗證碼驗證失敗"})
return
}
// 驗證成功后刪除 Redis 中的數(shù)據(jù)(防止重復使用)
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",
})
// 創(chuàng)建 Gin 路由
r := gin.Default()
// 創(chuàng)建處理器
captchaHandler := handler.NewCaptchaHandler(rdb)
// 注冊路由
api := r.Group("/api")
{
api.POST("/captcha", captchaHandler.GetCaptcha)
api.POST("/captcha/verify", captchaHandler.VerifyCaptcha)
}
r.Run(":8080")
}二、前端實現(xiàn)
2.1 安裝依賴
npm install go-captcha-vue npm install element-plus
2.2 創(chuàng)建驗證碼組件
創(chuàng)建 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: '發(fā)送驗證碼'
}
})
// Emits
const emit = defineEmits(['success'])
// 狀態(tài)
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
}
}
// 點擊按鈕觸發(fā)
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()
})
}
// 暴露方法供父組件調(diào)用
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('')
// 驗證碼驗證成功后的回調(diào)
const handleCaptchaSuccess = () => {
// 發(fā)送短信驗證碼
httpPost('/api/sms/send', { mobile: mobile.value })
.then(() => {
ElMessage.success('驗證碼已發(fā)送')
})
.catch((e) => {
ElMessage.error('發(fā)送失?。? + e.message)
})
}
const handleLogin = () => {
// 登錄邏輯
console.log('登錄', mobile.value, code.value)
}
</script>2.4 HTTP 工具函數(shù)
創(chuàng)建 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 驗證碼生成流程
- 前端點擊"獲取驗證碼"按鈕
- 前端調(diào)用
/api/captcha接口 - 后端生成驗證碼圖片和答案坐標
- 后端將完整數(shù)據(jù)(包含答案)存儲到 Redis,有效期1分鐘
- 后端返回驗證碼ID和圖片數(shù)據(jù)(不包含答案)給前端
- 前端展示滑動驗證碼彈窗
3.2 驗證碼驗證流程
- 用戶拖動滑塊到目標位置
- 前端獲取滑塊坐標,調(diào)用
/api/captcha/verify接口 - 后端從 Redis 獲取驗證碼答案數(shù)據(jù)
- 后端比對用戶滑動坐標與答案坐標(允許一定偏差)
- 驗證成功后刪除 Redis 數(shù)據(jù),返回成功響應
- 前端收到成功響應后執(zhí)行后續(xù)業(yè)務邏輯
3.3 安全性說明
- 答案不暴露:驗證碼答案坐標只存儲在服務端 Redis 中,不返回給前端
- 一次性使用:驗證成功后立即刪除 Redis 數(shù)據(jù),防止重復使用
- 時效性:驗證碼有效期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 項目中實現(xiàn)滑動驗證碼功能。核心要點:
- 后端使用 go-captcha 生成驗證碼圖片和答案
- 使用 Redis 存儲驗證碼數(shù)據(jù),保證安全性和時效性
- 前端使用 go-captcha-vue 組件展示驗證碼
- 驗證流程簡單清晰,用戶體驗良好
完整代碼可以直接應用到新項目中,只需根據(jù)實際情況調(diào)整路由、響應格式等細節(jié)即可。
參考資源
到此這篇關于Go + Vue 實現(xiàn)行為驗證碼完整指南的文章就介紹到這了,更多相關go vue行為驗證碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
vue-week-picker實現(xiàn)支持按周切換的日歷
這篇文章主要為大家詳細介紹了vue-week-picker實現(xiàn)支持按周切換的日歷,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-06-06
Vuex state中同步數(shù)據(jù)和異步數(shù)據(jù)方式
這篇文章主要介紹了Vuex state中同步數(shù)據(jù)和異步數(shù)據(jù)方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08
Vue3將虛擬節(jié)點渲染到網(wǎng)頁初次渲染詳解
這篇文章主要為大家介紹了Vue3將虛擬節(jié)點渲染到網(wǎng)頁初次渲染詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03
Vue2實現(xiàn)未登錄攔截頁面功能的基本步驟和示例代碼
在Vue 2中實現(xiàn)未登錄攔截頁面功能,通??梢酝ㄟ^路由守衛(wèi)和全局前置守衛(wèi)來完成,以下是一個基本的實現(xiàn)步驟和示例代碼,幫助你創(chuàng)建一個簡單的未登錄攔截邏輯,需要的朋友可以參考下2024-04-04

