Go + Vue 實(shí)現(xiàn)行為驗(yàn)證碼完整指南
前言
在現(xiàn)代 Web 應(yīng)用中,驗(yàn)證碼是防止機(jī)器人攻擊和惡意請求的重要手段。相比傳統(tǒng)的圖形驗(yàn)證碼,滑動(dòng)行為驗(yàn)證碼具有更好的用戶體驗(yàn)。本文將介紹如何使用 go-captcha 庫在 Go 后端和 Vue 前端實(shí)現(xiàn)滑動(dòng)驗(yàn)證碼功能。
技術(shù)棧
- 后端:Go + Gin 框架
- 前端:Vue 3 + Element Plus
- 驗(yàn)證碼庫:go-captcha(后端)+ go-captcha-vue(前端)
- 緩存:Redis(用于存儲(chǔ)驗(yàn)證碼數(shù)據(jù))
一、后端實(shí)現(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 初始化驗(yàn)證碼模塊
創(chuàng)建 captcha/init.go 文件:
package captcha
import (
"errors"
)
var (
ErrGenData = errors.New("generate data error")
)
const (
Deviation = 10 // 驗(yàn)證偏差值,允許用戶滑動(dòng)有一定誤差
)
func Init() error {
return initSlide()
}1.3 實(shí)現(xiàn)滑動(dòng)驗(yà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 初始化滑動(dòng)驗(yàn)證碼
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,
})
}
// 設(shè)置資源
builder.SetResources(
slide.WithGraphImages(newGraphs),
slide.WithBackgrounds(imgs),
)
slideCapt = builder.Make()
return nil
}
// Slide 驗(yàn)證碼數(shù)據(jù)結(jié)構(gòu)
type Slide struct {
// 滑塊初始顯示坐標(biāo)
SliderX int
SliderY int
SliderWidth int
SliderHeight int
// 整體大小
MainWidth int
MainHeight int
// 答案坐標(biāo)(服務(wù)端保存,不返回給前端)
X int
Y int
// 主圖與滑塊的圖片,base64編碼
MainImage string
SliderImage string
}
// NewSlide 生成新的滑動(dòng)驗(yàn)證碼
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
}
// 答案坐標(biāo)(正確的滑塊位置)
m.X = dotData.X
m.Y = dotData.Y
// 滑塊初始顯示坐標(biāo)
m.SliderWidth = dotData.Width
m.SliderHeight = dotData.Height
m.SliderX = dotData.DX
m.SliderY = dotData.DY
// 圖片大小
m.MainWidth = 300
m.MainHeight = 220
// 轉(zhuǎn)換為 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 驗(yàn)證滑動(dòng)位置是否正確
func VerifySlide(userX, userY, slideX, slideY int) bool {
return slide.Validate(userX, userY, slideX, slideY, Deviation)
}1.4 定義響應(yīng)結(jié)構(gòu)體
創(chuàng)建 vo/captcha.go 文件:
package vo
type GetCaptchaRes struct {
ID string `json:"id"` // 驗(yàn)證碼唯一標(biāo)識(shí)
SliderX int `json:"sliderX"` // 滑塊初始X坐標(biāo)
SliderY int `json:"sliderY"` // 滑塊初始Y坐標(biāo)
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 實(shí)現(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 獲取驗(yàn)證碼
func (h *CaptchaHandler) GetCaptcha(c *gin.Context) {
// 生成驗(yàn)證碼
m, err := captcha.NewSlide()
if err != nil {
c.JSON(500, gin.H{"error": "Failed to create captcha"})
return
}
// 序列化驗(yàn)證碼數(shù)據(jù)
value, err := json.Marshal(m)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to marshal captcha"})
return
}
// 生成唯一ID并存儲(chǔ)到 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ù)(不包含答案坐標(biāo))
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 驗(yàn)證滑動(dòng)驗(yàn)證碼
func (h *CaptchaHandler) VerifyCaptcha(c *gin.Context) {
var data struct {
ID string `json:"id"` // 驗(yàn)證碼標(biāo)識(shí)
X int `json:"x"` // 用戶滑動(dòng)的X坐標(biāo)
Y int `json:"y"` // 用戶滑動(dòng)的Y坐標(biāo)
}
if err := c.ShouldBindJSON(&data); err != nil {
c.JSON(400, gin.H{"error": "Invalid arguments"})
return
}
// 從 Redis 獲取驗(yàn)證碼數(shù)據(jù)
value, err := h.redis.Get(c, data.ID).Result()
if err != nil {
c.JSON(400, gin.H{"error": "驗(yàn)證碼已過期或不存在"})
return
}
// 反序列化驗(yàn)證碼數(shù)據(jù)
slide := captcha.Slide{}
err = json.Unmarshal([]byte(value), &slide)
if err != nil {
c.JSON(500, gin.H{"error": "驗(yàn)證碼數(shù)據(jù)錯(cuò)誤"})
return
}
// 驗(yàn)證滑動(dòng)位置
if !captcha.VerifySlide(data.X, data.Y, slide.X, slide.Y) {
c.JSON(400, gin.H{"error": "驗(yàn)證碼驗(yàn)證失敗"})
return
}
// 驗(yàn)證成功后刪除 Redis 中的數(shù)據(jù)(防止重復(fù)使用)
h.redis.Del(c, data.ID)
c.JSON(200, gin.H{"code": 0, "message": "驗(yàn)證成功"})
}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() {
// 初始化驗(yàn)證碼模塊
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")
}二、前端實(shí)現(xiàn)
2.1 安裝依賴
npm install go-captcha-vue npm install element-plus
2.2 創(chuàng)建驗(yàn)證碼組件
創(chuàng)建 components/SlideCaptcha.vue 文件:
<template>
<div class="slide-captcha-wrapper">
<el-button
:disabled="!canSend"
@click="handleClick"
class="trigger-btn"
>
{{ btnText }}
</el-button>
<!-- 滑動(dòng)驗(yàn)證碼彈窗 -->
<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ā)送驗(yàn)證碼'
}
})
// 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)
// 滑動(dòng)驗(yàn)證碼配置
const captchaConfig = {
width: 300,
height: 220,
thumbWidth: 60,
thumbHeight: 60,
showTheme: true,
title: '請拖動(dòng)滑塊完成驗(yàn)證'
}
// 滑動(dòng)驗(yàn)證碼事件
const captchaEvents = {
confirm: (point, reset) => {
verifyCaptcha({
x: Math.floor(point.x),
y: Math.floor(point.y)
})
return false
},
refresh: () => {
loadCaptcha()
},
close: () => {
showDialog.value = false
}
}
// 點(diǎn)擊按鈕觸發(fā)
const handleClick = () => {
if (!canSend.value) return
loadCaptcha()
}
// 加載滑動(dòng)驗(yàn)證碼
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('獲取驗(yàn)證碼失?。? + e.message)
})
}
// 驗(yàn)證滑動(dòng)驗(yàn)證碼
const verifyCaptcha = (verifyData) => {
httpPost('/api/captcha/verify', {
id: captchaKey.value,
x: verifyData.x,
y: verifyData.y
})
.then(() => {
showDialog.value = false
ElMessage.success('驗(yàn)證成功')
emit('success')
})
.catch((e) => {
ElMessage.error('驗(yàn)證失?。? + e.message)
// 驗(yàn)證失敗,重新加載驗(yàn)證碼
captchaData.value = null
loadCaptcha()
})
}
// 暴露方法供父組件調(diào)用
defineExpose({
loadCaptcha
})
</script>
<style scoped>
.slide-captcha-wrapper {
display: inline-block;
}
.trigger-btn {
width: 100%;
}
</style>2.3 使用驗(yàn)證碼組件
在需要使用驗(yàn)證碼的頁面中:
<template>
<div class="login-form">
<el-form>
<el-form-item label="手機(jī)號(hào)">
<el-input v-model="mobile" placeholder="請輸入手機(jī)號(hào)" />
</el-form-item>
<el-form-item label="驗(yàn)證碼">
<el-input v-model="code" placeholder="請輸入驗(yàn)證碼">
<template #append>
<SlideCaptcha
@success="handleCaptchaSuccess"
btn-text="獲取驗(yàn)證碼"
/>
</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('')
// 驗(yàn)證碼驗(yàn)證成功后的回調(diào)
const handleCaptchaSuccess = () => {
// 發(fā)送短信驗(yàn)證碼
httpPost('/api/sms/send', { mobile: mobile.value })
.then(() => {
ElMessage.success('驗(yàn)證碼已發(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)
}
)
// 響應(yīng)攔截器
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 驗(yàn)證碼生成流程
- 前端點(diǎn)擊"獲取驗(yàn)證碼"按鈕
- 前端調(diào)用
/api/captcha接口 - 后端生成驗(yàn)證碼圖片和答案坐標(biāo)
- 后端將完整數(shù)據(jù)(包含答案)存儲(chǔ)到 Redis,有效期1分鐘
- 后端返回驗(yàn)證碼ID和圖片數(shù)據(jù)(不包含答案)給前端
- 前端展示滑動(dòng)驗(yàn)證碼彈窗
3.2 驗(yàn)證碼驗(yàn)證流程
- 用戶拖動(dòng)滑塊到目標(biāo)位置
- 前端獲取滑塊坐標(biāo),調(diào)用
/api/captcha/verify接口 - 后端從 Redis 獲取驗(yàn)證碼答案數(shù)據(jù)
- 后端比對用戶滑動(dòng)坐標(biāo)與答案坐標(biāo)(允許一定偏差)
- 驗(yàn)證成功后刪除 Redis 數(shù)據(jù),返回成功響應(yīng)
- 前端收到成功響應(yīng)后執(zhí)行后續(xù)業(yè)務(wù)邏輯
3.3 安全性說明
- 答案不暴露:驗(yàn)證碼答案坐標(biāo)只存儲(chǔ)在服務(wù)端 Redis 中,不返回給前端
- 一次性使用:驗(yàn)證成功后立即刪除 Redis 數(shù)據(jù),防止重復(fù)使用
- 時(shí)效性:驗(yàn)證碼有效期1分鐘,過期自動(dòng)失效
- 偏差容忍:允許用戶滑動(dòng)有一定誤差(默認(rèn)10像素),提升用戶體驗(yàn)
四、常見問題
4.1 驗(yàn)證碼圖片不顯示
檢查 base64 編碼是否正確,確保前端正確解析 data:image/png;base64, 前綴。
4.2 驗(yàn)證總是失敗
檢查偏差值設(shè)置是否合理,可以適當(dāng)增大 Deviation 常量的值。
4.3 Redis 連接失敗
確保 Redis 服務(wù)已啟動(dòng),檢查連接地址和端口是否正確。
4.4 跨域問題
在 Gin 中添加 CORS 中間件:
import "github.com/gin-contrib/cors" r.Use(cors.Default())
五、總結(jié)
本文介紹了如何使用 go-captcha 庫在 Go + Vue 項(xiàng)目中實(shí)現(xiàn)滑動(dòng)驗(yàn)證碼功能。核心要點(diǎn):
- 后端使用 go-captcha 生成驗(yàn)證碼圖片和答案
- 使用 Redis 存儲(chǔ)驗(yàn)證碼數(shù)據(jù),保證安全性和時(shí)效性
- 前端使用 go-captcha-vue 組件展示驗(yàn)證碼
- 驗(yàn)證流程簡單清晰,用戶體驗(yàn)良好
完整代碼可以直接應(yīng)用到新項(xiàng)目中,只需根據(jù)實(shí)際情況調(diào)整路由、響應(yīng)格式等細(xì)節(jié)即可。
參考資源
到此這篇關(guān)于Go + Vue 實(shí)現(xiàn)行為驗(yàn)證碼完整指南的文章就介紹到這了,更多相關(guān)go vue行為驗(yàn)證碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Go+Vue開發(fā)一個(gè)線上外賣應(yīng)用的流程(用戶名密碼和圖形驗(yàn)證碼)
- django-simple-captcha多種驗(yàn)證碼的實(shí)現(xiàn)方法
- golang qq郵件發(fā)送驗(yàn)證碼功能
- 使用Go實(shí)現(xiàn)郵箱驗(yàn)證碼API功能
- Golang圖片驗(yàn)證碼的使用方法
- Go生成base64圖片驗(yàn)證碼實(shí)例(超詳細(xì)工具類)
- Vue實(shí)現(xiàn)驗(yàn)證碼登錄全過程
- vue3純前端實(shí)現(xiàn)驗(yàn)證碼代碼示例
- Vue實(shí)現(xiàn)驗(yàn)證碼登錄的超詳細(xì)步驟
相關(guān)文章
vue-week-picker實(shí)現(xiàn)支持按周切換的日歷
這篇文章主要為大家詳細(xì)介紹了vue-week-picker實(shí)現(xiàn)支持按周切換的日歷,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-06-06
VueJS設(shè)計(jì)與實(shí)現(xiàn)之淺響應(yīng)與深響應(yīng)詳解
這篇文章主要為大家介紹了VueJS設(shè)計(jì)與實(shí)現(xiàn)之淺響應(yīng)與深響應(yīng)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
Vuex state中同步數(shù)據(jù)和異步數(shù)據(jù)方式
這篇文章主要介紹了Vuex state中同步數(shù)據(jù)和異步數(shù)據(jù)方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08
Vue3將虛擬節(jié)點(diǎn)渲染到網(wǎng)頁初次渲染詳解
這篇文章主要為大家介紹了Vue3將虛擬節(jié)點(diǎn)渲染到網(wǎng)頁初次渲染詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
Vue2實(shí)現(xiàn)未登錄攔截頁面功能的基本步驟和示例代碼
在Vue 2中實(shí)現(xiàn)未登錄攔截頁面功能,通??梢酝ㄟ^路由守衛(wèi)和全局前置守衛(wèi)來完成,以下是一個(gè)基本的實(shí)現(xiàn)步驟和示例代碼,幫助你創(chuàng)建一個(gè)簡單的未登錄攔截邏輯,需要的朋友可以參考下2024-04-04
vue-router history模式下的微信分享小結(jié)
本篇文章主要介紹了vue-router history模式下的微信分享小結(jié),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-07-07

