优化验证码,加入干扰字

This commit is contained in:
xh
2025-09-18 18:41:38 +08:00
parent 31add6f13e
commit 8bd910e6a9
13 changed files with 167 additions and 182 deletions

View File

@@ -1,6 +1,7 @@
{
"cSpell.words": [
"autosize",
"colornames",
"daterange",
"datetime",
"dcloudio",

View File

@@ -4,40 +4,42 @@ import (
"image/color"
"x_admin/app/schema/commonSchema"
"x_admin/core"
"x_admin/util/aj-captcha-go/config"
"x_admin/util/aj-captcha-go/constant"
"x_admin/util/aj-captcha-go/service"
"x_admin/util/aj-captcha-go/captcha_config"
"x_admin/util/aj-captcha-go/captcha_service"
)
// var captcha_config = config.NewConfig()// 默认配置,可以根据项目自行配置,将其他类型配置序列化上去
var captcha_config = config.Config{
CacheType: constant.RedisCacheKey,
Watermark: &config.WatermarkConfig{
// var captcha_config = config.NewMemCacheConfig()// 默认配置,可以根据项目自行配置,将其他类型配置序列化上去
var captchaConfig = captcha_config.Config{
CacheType: captcha_config.RedisCacheKey,
Watermark: &captcha_config.WatermarkConfig{
FontSize: 12,
Color: color.RGBA{R: 255, G: 255, B: 255, A: 255},
Text: "admin",
Text: "",
},
ClickWord: &config.ClickWordConfig{
FontSize: 25,
FontNum: 4,
ClickWord: &captcha_config.ClickWordConfig{
FontSize: 22,
FontNum: 3,
AllFontNum: 7,
XOffset: 8,
YOffset: 8,
},
BlockPuzzle: &config.BlockPuzzleConfig{Offset: 10},
CacheExpireSec: 2 * 60, // 缓存有效时间
BlockPuzzle: &captcha_config.BlockPuzzleConfig{Offset: 8},
CacheExpireSec: 210 * 60, // 缓存有效时间
}
// 服务工厂,主要用户注册 获取 缓存和验证服务
var factory = service.NewCaptchaServiceFactory(&captcha_config)
var factory = captcha_service.NewCaptchaServiceFactory(&captchaConfig)
func init() {
// 这里默认是注册了 内存缓存,但是不足以应对生产环境,希望自行注册缓存驱动 实现缓存接口即可替换CacheType就是注册进去的 key
// factory.RegisterCache(constant.MemCacheKey, service.NewMemCacheService(200000)) // 这里20指的是缓存阈值
// 这里默认是注册了 内存缓存,但是不足以应对生产环境
// factory.RegisterCache(captcha_config.MemCacheKey, captcha_service.NewMemCacheService(200000)) // 这里20指的是缓存阈值
// //注册自定义配置redis数据库
factory.RegisterCache(constant.RedisCacheKey, service.NewConfigRedisCacheService(core.Redis))
factory.RegisterCache(captcha_config.RedisCacheKey, captcha_service.NewConfigRedisCacheService(core.Redis))
// 注册了两种验证码服务 可以自行实现更多的验证
factory.RegisterService(constant.ClickWordCaptcha, service.NewClickWordCaptchaService(factory))
factory.RegisterService(constant.BlockPuzzleCaptcha, service.NewBlockPuzzleCaptchaService(factory))
factory.RegisterService(captcha_config.ClickWordCaptcha, captcha_service.NewClickWordCaptchaService(factory))
factory.RegisterService(captcha_config.BlockPuzzleCaptcha, captcha_service.NewBlockPuzzleCaptchaService(factory))
}
func CaptchaGet(captchaType string) (interface{}, error) {

View File

@@ -0,0 +1,61 @@
package captcha_config
import (
"image/color"
)
// WatermarkConfig 水印设置
type WatermarkConfig struct {
FontSize int
Color color.RGBA
Text string // 水印文字
}
// 滑块配置
type BlockPuzzleConfig struct {
Offset int // 校验时 容错偏移量
}
// 点击文字配置
type ClickWordConfig struct {
FontSize int // 点击文字字体大小
FontNum int // 点击文字数量
AllFontNum int // 点击文字显示数量
XOffset int // 点击文字X轴偏移量
YOffset int // 点击文字Y轴偏移量
}
type Config struct {
CacheType string // 验证码使用的缓存类型
CacheExpireSec int // 缓存有效时间
Watermark *WatermarkConfig // 水印配置
ClickWord *ClickWordConfig // 点击文字配置
BlockPuzzle *BlockPuzzleConfig // 滑动模块配置
}
// 默认验证码配置
func NewMemCacheConfig() *Config {
return &Config{
//可以为redis类型缓存RedisCacheKey也可以为内存MemCacheKey
CacheType: MemCacheKey,
CacheExpireSec: 2 * 60, // 缓存有效时间
// 水印配置
Watermark: &WatermarkConfig{
FontSize: 12,
Color: color.RGBA{R: 255, G: 255, B: 255, A: 255},
Text: "x_admin",
},
// 点击文字配置(参数可从业务系统自定义)
ClickWord: &ClickWordConfig{
FontSize: 25,
FontNum: 4,
AllFontNum: 10,
XOffset: 10,
YOffset: 10,
},
// 滑动模块配置(参数可从业务系统自定义)
BlockPuzzle: &BlockPuzzleConfig{Offset: 10},
}
}

View File

@@ -1,4 +1,4 @@
package constant
package captcha_config
const (
// CodeKeyPrefix 缓存key前缀

View File

@@ -1,4 +1,4 @@
package service
package captcha_service
type CacheCaptchaInterface interface {
Get(key string) string

View File

@@ -1,4 +1,4 @@
package service
package captcha_service
import (
"strconv"

View File

@@ -1,4 +1,4 @@
package service
package captcha_service
import (
"strconv"

View File

@@ -1,4 +1,4 @@
package service
package captcha_service
type CaptchaInterface interface {

View File

@@ -1,4 +1,4 @@
package service
package captcha_service
import (
"encoding/json"
@@ -6,7 +6,7 @@ import (
"fmt"
"log"
"math"
"x_admin/util/aj-captcha-go/constant"
"x_admin/util/aj-captcha-go/captcha_config"
"x_admin/util/aj-captcha-go/model/vo"
"x_admin/util/aj-captcha-go/util"
img "x_admin/util/aj-captcha-go/util/image"
@@ -35,8 +35,9 @@ func (b *BlockPuzzleCaptchaService) Get() (map[string]interface{}, error) {
backgroundImage := img.GetBackgroundImage()
// 为背景图片设置水印
// backgroundImage.SetText(b.factory.config.Watermark.Text, b.factory.config.Watermark.FontSize, b.factory.config.Watermark.Color)
if b.factory.config.Watermark.Text != "" {
backgroundImage.SetText(b.factory.config.Watermark.Text, b.factory.config.Watermark.FontSize, b.factory.config.Watermark.Color)
}
// 初始化模板图片
templateImage := img.GetTemplateImage()
@@ -58,7 +59,7 @@ func (b *BlockPuzzleCaptchaService) Get() (map[string]interface{}, error) {
data["secretKey"] = b.point.SecretKey
data["token"] = util.GetUuid()
codeKey := fmt.Sprintf(constant.CodeKeyPrefix, data["token"])
codeKey := fmt.Sprintf(captcha_config.CodeKeyPrefix, data["token"])
jsonPoint, err := json.Marshal(b.point)
if err != nil {
log.Printf("point json Marshal err: %v", err)
@@ -190,7 +191,7 @@ func (b *BlockPuzzleCaptchaService) generateJigsawPoint(backgroundImage *util.Im
func (b *BlockPuzzleCaptchaService) Check(token string, pointJson string) error {
cache := b.factory.GetCache()
codeKey := fmt.Sprintf(constant.CodeKeyPrefix, token)
codeKey := fmt.Sprintf(captcha_config.CodeKeyPrefix, token)
cachePointInfo := cache.Get(codeKey)
@@ -229,7 +230,7 @@ func (b *BlockPuzzleCaptchaService) Verification(token string, pointJson string)
if err != nil {
return err
}
codeKey := fmt.Sprintf(constant.CodeKeyPrefix, token)
codeKey := fmt.Sprintf(captcha_config.CodeKeyPrefix, token)
b.factory.GetCache().Delete(codeKey)
return nil
}

View File

@@ -1,11 +1,11 @@
package service
package captcha_service
import (
"encoding/json"
"errors"
"fmt"
"log"
"x_admin/util/aj-captcha-go/constant"
"x_admin/util/aj-captcha-go/captcha_config"
"x_admin/util/aj-captcha-go/model/vo"
"x_admin/util/aj-captcha-go/util"
img "x_admin/util/aj-captcha-go/util/image"
@@ -24,41 +24,10 @@ type ClickWordCaptchaService struct {
factory *CaptchaServiceFactory
}
func (c *ClickWordCaptchaService) Get() (map[string]interface{}, error) {
// 初始化背景图片
backgroundImage := img.GetClickBackgroundImage()
pointList, wordList, err := c.getImageData(backgroundImage)
if err != nil {
return nil, err
}
originalImageBase64, err := backgroundImage.Base64()
if err != nil {
return nil, err
}
data := make(map[string]interface{})
data["originalImageBase64"] = originalImageBase64
data["wordList"] = wordList
data["secretKey"] = pointList[0].SecretKey
data["token"] = util.GetUuid()
codeKey := fmt.Sprintf(constant.CodeKeyPrefix, data["token"])
jsonPoint, err := json.Marshal(pointList)
if err != nil {
log.Printf("point json Marshal err: %v", err)
return nil, err
}
c.factory.GetCache().Set(codeKey, string(jsonPoint), c.factory.config.CacheExpireSec)
return data, nil
}
// 校验点击文字验证码
func (c *ClickWordCaptchaService) Check(token string, pointJson string) error {
cache := c.factory.GetCache()
codeKey := fmt.Sprintf(constant.CodeKeyPrefix, token)
codeKey := fmt.Sprintf(captcha_config.CodeKeyPrefix, token)
cachePointInfo := cache.Get(codeKey)
@@ -85,11 +54,13 @@ func (c *ClickWordCaptchaService) Check(token string, pointJson string) error {
if err != nil {
return err
}
XOffset := c.factory.config.ClickWord.XOffset
YOffset := c.factory.config.ClickWord.YOffset
fontSize := c.factory.config.ClickWord.FontSize
for i, pointVO := range cachePoint {
targetPoint := userPoint[i]
if targetPoint.X >= pointVO.X-15 && targetPoint.X <= pointVO.X+fontSize+15 && targetPoint.Y >= pointVO.Y-15 && targetPoint.Y <= pointVO.Y+fontSize+15 {
if targetPoint.X >= pointVO.X-XOffset && targetPoint.X <= pointVO.X+fontSize+XOffset && targetPoint.Y >= pointVO.Y-YOffset && targetPoint.Y <= pointVO.Y+fontSize+YOffset {
} else {
return errors.New("验证失败")
@@ -99,47 +70,82 @@ func (c *ClickWordCaptchaService) Check(token string, pointJson string) error {
return nil
}
// 校验点击文字验证码-并删除
func (c *ClickWordCaptchaService) Verification(token string, pointJson string) error {
err := c.Check(token, pointJson)
if err != nil {
return err
}
codeKey := fmt.Sprintf(constant.CodeKeyPrefix, token)
codeKey := fmt.Sprintf(captcha_config.CodeKeyPrefix, token)
c.factory.GetCache().Delete(codeKey)
return nil
}
func (c *ClickWordCaptchaService) getImageData(image *util.ImageUtil) ([]vo.PointVO, []string, error) {
wordCount := c.factory.config.ClickWord.FontNum
func (c *ClickWordCaptchaService) Get() (map[string]interface{}, error) {
// 初始化背景图片
backgroundImage := img.GetClickBackgroundImage()
// 为背景图片设置水印
if c.factory.config.Watermark.Text != "" {
backgroundImage.SetText(c.factory.config.Watermark.Text, c.factory.config.Watermark.FontSize, c.factory.config.Watermark.Color)
}
pointList, wordList, err := c.getImageData(backgroundImage)
if err != nil {
return nil, err
}
// 某个字不参与校验
num := util.RandomInt(1, wordCount)
currentWord := c.getRandomWords(wordCount)
originalImageBase64, err := backgroundImage.Base64()
if err != nil {
return nil, err
}
data := make(map[string]interface{})
data["originalImageBase64"] = originalImageBase64
data["wordList"] = wordList
data["secretKey"] = pointList[0].SecretKey
data["token"] = util.GetUuid()
codeKey := fmt.Sprintf(captcha_config.CodeKeyPrefix, data["token"])
jsonPoint, err := json.Marshal(pointList)
if err != nil {
log.Printf("point json Marshal err: %v", err)
return nil, err
}
c.factory.GetCache().Set(codeKey, string(jsonPoint), c.factory.config.CacheExpireSec)
return data, nil
}
func (c *ClickWordCaptchaService) getImageData(image *util.ImageUtil) ([]vo.PointVO, []string, error) {
AllFontNum := c.factory.config.ClickWord.AllFontNum
FontNum := c.factory.config.ClickWord.FontNum
AllWord := c.getRandomWords(AllFontNum)
// currentWord := AllWord[:FontNum]
var pointList []vo.PointVO
var wordList []string
i := 0
// 构建本次的 secret
key := util.RandString(16)
for _, s := range currentWord {
point := c.randomWordPoint(image.Width, image.Height, i, wordCount)
for k, s := range AllWord {
fontSize := util.RandomInt(c.factory.config.ClickWord.FontSize-2, c.factory.config.ClickWord.FontSize+2)
point := c.randomWordPoint(image.Width, image.Height, fontSize)
point.SetSecretKey(key)
// 随机设置文字 TODO 角度未设置
err := image.SetArtText(s, c.factory.config.ClickWord.FontSize, point)
err := image.SetArtText(s, fontSize, point)
if err != nil {
return nil, nil, err
}
if (num - 1) != i {
if k < FontNum {
pointList = append(pointList, point)
wordList = append(wordList, s)
}
i++
}
return pointList, wordList, nil
}
@@ -155,7 +161,7 @@ func (c *ClickWordCaptchaService) getRandomWords(count int) []string {
word := runesArray[util.RandomInt(0, size-1)]
set[string(word)] = true
if len(set) >= count {
for str, _ := range set {
for str := range set {
wordList = append(wordList, str)
}
break
@@ -164,20 +170,9 @@ func (c *ClickWordCaptchaService) getRandomWords(count int) []string {
return wordList
}
func (c *ClickWordCaptchaService) randomWordPoint(width int, height int, i int, count int) vo.PointVO {
avgWidth := width / (count + 1)
fontSizeHalf := c.factory.config.ClickWord.FontSize / 2
func (c *ClickWordCaptchaService) randomWordPoint(width int, height int, fontSize int) vo.PointVO {
var x, y int
if avgWidth < fontSizeHalf {
x = util.RandomInt(1+fontSizeHalf, width)
} else {
if i == 0 {
x = util.RandomInt(1+fontSizeHalf, avgWidth*(i+1)-fontSizeHalf)
} else {
x = util.RandomInt(avgWidth*i+fontSizeHalf, avgWidth*(i+1)-fontSizeHalf)
}
}
y = util.RandomInt(c.factory.config.ClickWord.FontSize, height-fontSizeHalf)
x := util.RandomInt(fontSize, width-fontSize)
y := util.RandomInt(fontSize, height-fontSize)
return vo.PointVO{X: x, Y: y}
}

View File

@@ -1,12 +1,12 @@
package service
package captcha_service
import (
"log"
"sync"
configIns "x_admin/util/aj-captcha-go/config"
"x_admin/util/aj-captcha-go/captcha_config"
)
func NewCaptchaServiceFactory(config *configIns.Config) *CaptchaServiceFactory {
func NewCaptchaServiceFactory(config *captcha_config.Config) *CaptchaServiceFactory {
factory := &CaptchaServiceFactory{
ServiceMap: make(map[string]CaptchaInterface),
@@ -18,7 +18,7 @@ func NewCaptchaServiceFactory(config *configIns.Config) *CaptchaServiceFactory {
// CaptchaServiceFactory 验证码服务工厂
type CaptchaServiceFactory struct {
config *configIns.Config
config *captcha_config.Config
ServiceMap map[string]CaptchaInterface
ServiceLock sync.RWMutex

View File

@@ -1,75 +0,0 @@
package config
import (
"image/color"
"x_admin/util/aj-captcha-go/constant"
)
// WatermarkConfig 水印设置
type WatermarkConfig struct {
FontSize int `yaml:"fontSize"`
Color color.RGBA `yaml:"color"`
Text string `yaml:"text"`
}
type BlockPuzzleConfig struct {
// 校验时 容错偏移量
Offset int `yaml:"offset"`
}
type ClickWordConfig struct {
FontSize int `yaml:"fontSize"`
FontNum int `yaml:"fontNum"`
}
// RedisConfig redis配置选项
type RedisConfig struct {
//redis单机或者集群访问地址
DBAddress []string `yaml:"dbAddress"`
//最大空闲连接数
DBMaxIdle int `yaml:"dbMaxIdle"`
//最大连接数
DBMaxActive int `yaml:"dbMaxActive"`
//redis表示空闲连接保活时间
DBIdleTimeout int `yaml:"dbIdleTimeout"`
//redis用户
DBUserName string `yaml:"dbUserName"`
//redis密码
DBPassWord string `yaml:"dbPassWord"`
//是否使用redis集群
EnableCluster bool `yaml:"enableCluster"`
//单机模式下使用redis的指定库比如0123等等默认为0
DB int `yaml:"db"`
}
type Config struct {
// 验证码使用的缓存类型
CacheType string `yaml:"cacheType"`
CacheExpireSec int `yaml:"cacheExpireSec"`
Watermark *WatermarkConfig `yaml:"watermark"`
ClickWord *ClickWordConfig `yaml:"clickWord"`
BlockPuzzle *BlockPuzzleConfig `yaml:"blockPuzzle"`
}
// 默认验证码配置
func NewConfig() *Config {
return &Config{
//可以为redis类型缓存RedisCacheKey也可以为内存MemCacheKey
CacheType: constant.MemCacheKey,
// 水印配置
Watermark: &WatermarkConfig{
FontSize: 12,
Color: color.RGBA{R: 255, G: 255, B: 255, A: 255},
Text: "我的水印",
},
// 点击文字配置(参数可从业务系统自定义)
ClickWord: &ClickWordConfig{
FontSize: 25,
FontNum: 4,
},
// 滑动模块配置(参数可从业务系统自定义)
BlockPuzzle: &BlockPuzzleConfig{Offset: 10},
CacheExpireSec: 2 * 60, // 缓存有效时间
}
}

View File

@@ -4,7 +4,7 @@ import (
"log"
"os"
"path/filepath"
"x_admin/util/aj-captcha-go/constant"
"x_admin/util/aj-captcha-go/captcha_config"
"x_admin/util/aj-captcha-go/util"
)
@@ -14,9 +14,9 @@ var templateImageArr []string
func SetUp() {
backgroundImageRoot := constant.DefaultResourceRoot + constant.DefaultBackgroundImageDirectory
templateImageRoot := constant.DefaultResourceRoot + constant.DefaultTemplateImageDirectory
clickBackgroundImageRoot := constant.DefaultResourceRoot + constant.DefaultClickBackgroundImageDirectory
backgroundImageRoot := captcha_config.DefaultResourceRoot + captcha_config.DefaultBackgroundImageDirectory
templateImageRoot := captcha_config.DefaultResourceRoot + captcha_config.DefaultTemplateImageDirectory
clickBackgroundImageRoot := captcha_config.DefaultResourceRoot + captcha_config.DefaultClickBackgroundImageDirectory
err1 := filepath.Walk(backgroundImageRoot, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
@@ -59,7 +59,7 @@ func GetBackgroundImage() *util.ImageUtil {
if max <= 0 {
max = 1
}
return util.NewImageUtil(backgroundImageArr[util.RandomInt(0, max)], constant.DefaultResourceRoot+constant.DefaultFont)
return util.NewImageUtil(backgroundImageArr[util.RandomInt(0, max)], captcha_config.DefaultResourceRoot+captcha_config.DefaultFont)
}
func GetTemplateImage() *util.ImageUtil {
@@ -67,7 +67,7 @@ func GetTemplateImage() *util.ImageUtil {
if max <= 0 {
max = 1
}
return util.NewImageUtil(templateImageArr[util.RandomInt(0, max)], constant.DefaultResourceRoot+constant.DefaultFont)
return util.NewImageUtil(templateImageArr[util.RandomInt(0, max)], captcha_config.DefaultResourceRoot+captcha_config.DefaultFont)
}
func GetClickBackgroundImage() *util.ImageUtil {
@@ -75,5 +75,5 @@ func GetClickBackgroundImage() *util.ImageUtil {
if max <= 0 {
max = 1
}
return util.NewImageUtil(clickBackgroundImageArr[util.RandomInt(0, max)], constant.DefaultResourceRoot+constant.DefaultFont)
return util.NewImageUtil(clickBackgroundImageArr[util.RandomInt(0, max)], captcha_config.DefaultResourceRoot+captcha_config.DefaultFont)
}