Gin接入Go-Captcha实现人机校验验证码获取和校验

6次阅读
没有评论

以前写项目,校验都用的图形验证码,这次用go体验了一下行为验证码

接下来我们要用到的go库是

GoCaptcha | 行为验证码

Gin接入Go-Captcha实现人机校验验证码获取和校验

它支持多种校验方式

安装Go-Captcha

安装模块

go get -u github.com/wenlng/go-captcha/v2@latest

安装预制静态资源

go get -u github.com/wenlng/go-captcha/v2@latest

初始化验证码

我参考作者文档和demo编写的以下方法

package common

import (
	"github.com/golang/freetype/truetype"
	"github.com/wenlng/go-captcha-assets/bindata/chars"
	"github.com/wenlng/go-captcha-assets/resources/fonts/fzshengsksjw"
	"github.com/wenlng/go-captcha-assets/resources/images"
	"github.com/wenlng/go-captcha-assets/resources/tiles"
	"github.com/wenlng/go-captcha/v2/base/option"
	"github.com/wenlng/go-captcha/v2/click"
	"github.com/wenlng/go-captcha/v2/rotate"
	"github.com/wenlng/go-captcha/v2/slide"
	"log"
)

// ClickCaptcha 全局点选验证码对象
var ClickCaptcha *click.Captcha

// SlideCaptcha 全局滑动验证码对象
var SlideCaptcha *slide.Captcha

// RotateCaptcha 全局旋转验证码对象
var RotateCaptcha *rotate.Captcha

// 验证码类型
const (
	CaptchaTypeClick  string = "click"
	CaptchaTypeSlide  string = "slide"
	CaptchaTypeRotate string = "rotate"
)

// InitClickCaptcha
// @Description 初始化点选验证码
// @Author Ham 2025-02-19 20:42:43
func InitClickCaptcha() {
	builder := click.NewBuilder()

	// fonts
	fonts, err := fzshengsksjw.GetFont()
	if err != nil {
		log.Fatalln(err)
	}

	// background images
	imgs, err := images.GetImages()
	if err != nil {
		log.Fatalln(err)
	}

	builder.SetResources(
		click.WithChars(chars.GetChineseChars()),
		click.WithFonts([]*truetype.Font{fonts}),
		click.WithBackgrounds(imgs),
	)

	textCapt := builder.Make()
	ClickCaptcha = &textCapt
}

// InitSlideCaptcha
// @Description 初始化滑动验证码
// @Author Ham 2025-02-19 20:45:06
func InitSlideCaptcha() {
	builder := slide.NewBuilder(
		//slide.WithGenGraphNumber(2),
		slide.WithEnableGraphVerticalRandom(true),
	)

	// background images
	imgs, err := images.GetImages()
	if err != nil {
		log.Fatalln(err)
	}

	graphs, err := tiles.GetTiles()
	if err != nil {
		log.Fatalln(err)
	}

	var newGraphs = make([]*slide.GraphImage, 0, len(graphs))
	for i := 0; i < len(graphs); i++ {
		graph := graphs[i]
		newGraphs = append(newGraphs, &slide.GraphImage{
			OverlayImage: graph.OverlayImage,
			MaskImage:    graph.MaskImage,
			ShadowImage:  graph.ShadowImage,
		})
	}

	// set resources
	builder.SetResources(
		slide.WithGraphImages(newGraphs),
		slide.WithBackgrounds(imgs),
	)

	slideCapt := builder.Make()
	SlideCaptcha = &slideCapt
}

// InitRotateCaptcha
// @Description 初始化旋转验证码
// @Author Ham 2025-02-19 20:50:14
func InitRotateCaptcha() {
	builder := rotate.NewBuilder(rotate.WithRangeAnglePos([]option.RangeVal{
		{Min: 20, Max: 330},
	}))

	// background images
	imgs, err := images.GetImages()
	if err != nil {
		log.Fatalln(err)
	}

	// set resources
	builder.SetResources(
		rotate.WithImages(imgs),
	)

	rotateCapt := builder.Make()
	RotateCaptcha = &rotateCapt
}

// InitCaptcha
// @Description 初始化验证码
// @Author Ham 2025-02-19 20:50:27
func InitCaptcha() {
	InitClickCaptcha()
	InitSlideCaptcha()
	InitRotateCaptcha()
}

之后在main方法里面,调用initCaptcha(),初始化验证码

验证码生成和校验工具

参考作者demo编写了以下代码,第一次看作者的demo可能有点一头雾水,没有注释,可以看看下面的

package util

import (
	"backend/internal/common"
)

// 生成验证码时调用下面的GenerateCaptcha方法,传入验证码类型,然后将captchaData返回给前端,用于生成验证码

// GenerateCaptcha
// @Description 生成验证码
// @Author Ham 2025-02-19 21:00:43
// @Param captchaType 验证码类型
// @Return captchaData 验证码数据
func GenerateCaptcha(captchaType string) (captchaData map[string]interface{}, err error) {
	switch captchaType {
	// 点选验证码
	case common.CaptchaTypeClick:
		{
			captcha := *common.ClickCaptcha
			generatedCaptcha, err := captcha.Generate()
			if err != nil {
				return nil, err
			}
			thumbImageBase64, err := generatedCaptcha.GetThumbImage().ToBase64()
			if err != nil {
				return nil, err
			}
			masterImageBase64, err := generatedCaptcha.GetMasterImage().ToBase64()
			if err != nil {
				return nil, err
			}
			captchaData = map[string]interface{}{
                                // 这里生成唯一的主键,用于存入缓存
                                "uuid": "",
				"thumbImage":  thumbImageBase64,
				"masterImage": masterImageBase64,
			}
	                // 根据业务实际,将下面的数据用上面的uuid存入缓存,用于之后校验时对比
	                data := generatedCaptcha.GetData()

			return captchaData, nil
		}

	//	滑动验证码
	case common.CaptchaTypeSlide:
		{
			captcha := *common.SlideCaptcha
			generatedCaptcha, err := captcha.Generate()
			if err != nil {
				return nil, err
			}
			tileImageBase64, err := generatedCaptcha.GetTileImage().ToBase64()
			if err != nil {
				return nil, err
			}
			masterImageBase64, err := generatedCaptcha.GetMasterImage().ToBase64()
			if err != nil {
				return nil, err
			}
			captchaData = map[string]interface{}{
                                // 这里生成唯一的主键,用于存入缓存
                                "uuid": "",
				"tileImage":   tileImageBase64,
				"masterImage": masterImageBase64,
                                "thumbX":      generatedCaptcha.GetData().TileX,
		                "thumbY":      generatedCaptcha.GetData().TileY,
		                "thumbWidth":  generatedCaptcha.GetData().Width,
		                "thumbHeight": generatedCaptcha.GetData().Height,
			}
                        // 根据业务实际,将下面的数据用上面的uuid存入缓存,用于之后校验时对比
	                data := generatedCaptcha.GetData()

			return captchaData, nil
		}

	// 旋转验证码
	case common.CaptchaTypeRotate:
		{
			captcha := *common.RotateCaptcha
			generatedCaptcha, err := captcha.Generate()
			if err != nil {
				return nil, err
			}
			thumbImageBase64, err := generatedCaptcha.GetThumbImage().ToBase64()
			if err != nil {
				return nil, err
			}
			masterImageBase64, err := generatedCaptcha.GetMasterImage().ToBase64()
			if err != nil {
				return nil, err
			}
			captchaData = map[string]interface{}{
                                // 这里生成唯一的主键,用于存入缓存
                                "uuid": "",
				"thumbImage":  thumbImageBase64,
				"masterImage": masterImageBase64,
			}
                        // 根据业务实际,将下面的数据用上面的uuid存入缓存,用于之后校验时对比
	                data := generatedCaptcha.GetData()

			return captchaData, nil
		}

	default:
		{
			return
		}
	}
}

// 下面是校验的演示,这里是用了redis,业务中,请替换自己的缓存逻辑,校验时调用ValidateCaptcha即可

// ValidateClickCaptcha
// @Description 校验点选验证码
// @Author Ham 2025-02-20 13:27:28
// @Param captchaData 验证码数据
// @Return validated 校验结果
// @Return err 错误
func ValidateClickCaptcha(captchaData map[string]interface{}) (validated bool, err error) {
	// 根据业务实际,从缓存中取出正确校验数据
	uuidString := captchaData["uuid"].(string)
	if uuidString == "" {
		return false, common.NewError("无效的验证码")
	}
	ctx := context.TODO()
	key := fmt.Sprintf(redisKey.RedisKeyCaptcha, uuidString)
	result, err := cache.RedisClient.Get(ctx, key).Result()
	if err != nil {
		err = common.NewError("无效的验证码")
		return
	}
	if result == "" {
		return false, common.NewError("无效的验证码")
	}

	// 从缓存结果解析正确校验数据
	rightCaptchaData := map[int]*click.Dot{}
	err = json.Unmarshal([]byte(result), &rightCaptchaData)
	if err != nil {
		return false, common.NewError("解析验证码失败")
	}

	// 校验验证码
	dotsInterface := captchaData["dots"].([]interface{})
	dots := make([]float64, len(dotsInterface))
	for i, v := range dotsInterface {
		dots[i], _ = v.(float64) // 将每个元素转换为float64
	}
	// 点数*2 为x,y坐标总数,数量应与请求传来的x,y坐标数相同
	if (len(rightCaptchaData) * 2) == len(dots) {
		for i := 0; i < len(rightCaptchaData); i++ {
			dot := rightCaptchaData[i]
			j := i * 2
			k := i*2 + 1
			sx, _ := strconv.ParseFloat(fmt.Sprintf("%v", dots[j]), 64)
			sy, _ := strconv.ParseFloat(fmt.Sprintf("%v", dots[k]), 64)

			validated = click.CheckPoint(int64(sx), int64(sy), int64(dot.X), int64(dot.Y), int64(dot.Width), int64(dot.Height), 0)
			if !validated {
				break
			}
		}
	}

	// 如果校验成功,根据业务实际删除验证码在缓存中的缓存
	if validated {
		cache.RedisClient.Del(ctx, key)
	}

	return
}

// ValidateSlideCaptcha
// @Description 校验滑动验证码
// @Author Ham 2025-02-20 13:27:28
// @Param captchaData 验证码数据
// @Return validated 校验结果
// @Return err 错误
func ValidateSlideCaptcha(captchaData map[string]interface{}) (validated bool, err error) {
	// 根据业务实际,从缓存中取出正确校验数据
	uuidString := captchaData["uuid"].(string)
	if uuidString == "" {
		return false, common.NewError("无效的验证码")
	}
	ctx := context.TODO()
	key := fmt.Sprintf(redisKey.RedisKeyCaptcha, uuidString)
	result, err := cache.RedisClient.Get(ctx, key).Result()
	if err != nil {
		err = common.NewError("无效的验证码")
		return
	}
	if result == "" {
		return false, common.NewError("无效的验证码")
	}

	// 从缓存结果解析正确校验数据
	var rightCaptchaData *slide.Block
	err = json.Unmarshal([]byte(result), &rightCaptchaData)
	if err != nil {
		return false, common.NewError("解析验证码失败")
	}

	// 校验验证码
	pointsInterface := captchaData["points"].([]interface{})
	points := make([]float64, len(pointsInterface))
	for i, v := range pointsInterface {
		points[i], _ = v.(float64) // 将每个元素转换为float64
	}
	if 2 == len(points) {
		sx, _ := strconv.ParseFloat(fmt.Sprintf("%v", points[0]), 64)
		sy, _ := strconv.ParseFloat(fmt.Sprintf("%v", points[1]), 64)
		validated = slide.CheckPoint(int64(sx), int64(sy), int64(rightCaptchaData.X), int64(rightCaptchaData.Y), 4)
	}

	// 如果校验成功,根据业务实际删除验证码在缓存中的缓存
	if validated {
		cache.RedisClient.Del(ctx, key)
	}

	return
}

// ValidateRotateCaptcha
// @Description 校验旋转验证码
// @Author Ham 2025-02-20 14:14:07
// @Param captchaData 验证码数据
// @Return validated 校验结果
// @Return err 错误
func ValidateRotateCaptcha(captchaData map[string]interface{}) (validated bool, err error) {
	// 根据业务实际,从缓存中取出正确校验数据
	uuidString := captchaData["uuid"].(string)
	if uuidString == "" {
		return false, common.NewError("无效的验证码")
	}
	ctx := context.TODO()
	key := fmt.Sprintf(redisKey.RedisKeyCaptcha, uuidString)
	result, err := cache.RedisClient.Get(ctx, key).Result()
	if err != nil {
		err = common.NewError("无效的验证码")
		return
	}
	if result == "" {
		return false, common.NewError("无效的验证码")
	}

	// 从缓存结果解析正确校验数据
	var rightCaptchaData *rotate.Block
	err = json.Unmarshal([]byte(result), &rightCaptchaData)
	if err != nil {
		return false, common.NewError("解析验证码失败")
	}

	// 校验验证码
	angle := captchaData["angle"].(float64)
	sAngle, _ := strconv.ParseFloat(fmt.Sprintf("%v", angle), 64)
	validated = rotate.CheckAngle(int64(sAngle), int64(rightCaptchaData.Angle), 2)

	// 如果校验成功,根据业务实际删除验证码在缓存中的缓存
	if validated {
		cache.RedisClient.Del(ctx, key)
	}

	return
}

// ValidateCaptcha
// @Description 校验验证码
// @Author Ham 2025-02-20 13:27:28
// @Param captchaType 验证码类型
// @Param captchaData 验证码数据
// @Return validated 校验结果
// @Return err 错误
func ValidateCaptcha(captchaType string, captchaData map[string]interface{}) (validated bool, err error) {
	switch captchaType {
	case CaptchaTypeClick:
		return ValidateClickCaptcha(captchaData)
	case CaptchaTypeSlide:
		return ValidateSlideCaptcha(captchaData)
	case CaptchaTypeRotate:
		return ValidateRotateCaptcha(captchaData)
	default:
		return false, common.NewError("不存在的验证码类型")
	}
}

以上三种校验方法的形参captchaData分别为

// 点选验证码:
{
    uuid (string),
    dots ([]int)
}

// 滑动验证码
{
    uuid (string),
    points ([]int)
}

// 旋转验证码
{
    uuid (string),
    angle (int)
}

校验时前端像上面这样传数据来校验就可以了

uuid在前端生成验证码时请求后端生成验证码接口获取,

dots,points,angle的计算方法参考下面每一个demo的confirmEvent方法

go-captcha-example/web/vue/src/hooks at master · wenlng/go-captcha-example

正文完
 1
评论(没有评论)
验证码