升级依赖,集成aj-captcha-go方便二开

This commit is contained in:
xiangheng
2024-02-25 22:01:41 +08:00
parent 0957f6c958
commit df43e4e7dd
21 changed files with 1490 additions and 50 deletions

View File

@@ -0,0 +1,120 @@
package config
import (
"errors"
"image/color"
"strings"
constant "x_admin/util/aj-captcha-go/const"
)
// 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 {
Watermark *WatermarkConfig `yaml:"watermark"`
ClickWord *ClickWordConfig `yaml:"clickWord"`
BlockPuzzle *BlockPuzzleConfig `yaml:"blockPuzzle"`
// 验证码使用的缓存类型
CacheType string `yaml:"cacheType"`
CacheExpireSec int `yaml:"cacheExpireSec"`
// 项目的绝对路径: 图片、字体等
ResourcePath string `yaml:"resourcePath"`
}
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, // 缓存有效时间
ResourcePath: "./",
}
}
// BuildConfig 生成config配置
func BuildConfig(cacheType, resourcePath string, waterConfig *WatermarkConfig, clickWordConfig *ClickWordConfig,
puzzleConfig *BlockPuzzleConfig, cacheExpireSec int) *Config {
if len(resourcePath) == 0 {
resourcePath = constant.DefaultResourceRoot
}
if len(cacheType) == 0 {
cacheType = constant.MemCacheKey
} else if strings.Compare(cacheType, constant.MemCacheKey) != 0 &&
strings.Compare(cacheType, constant.RedisCacheKey) != 0 {
panic(errors.New("cache type not support"))
return nil
}
if cacheExpireSec == 0 {
cacheExpireSec = 2 * 60
}
if nil == waterConfig {
waterConfig = &WatermarkConfig{
FontSize: 12,
Color: color.RGBA{R: 255, G: 255, B: 255, A: 255},
Text: constant.DefaultText,
}
}
if nil == clickWordConfig {
clickWordConfig = &ClickWordConfig{
FontSize: 25,
FontNum: 4,
}
}
if nil == puzzleConfig {
puzzleConfig = &BlockPuzzleConfig{Offset: 10}
}
return &Config{
//可以为redis类型缓存RedisCacheKey也可以为内存MemCacheKey
CacheType: cacheType,
Watermark: waterConfig,
ClickWord: clickWordConfig,
BlockPuzzle: puzzleConfig,
// 缓存有效时间
CacheExpireSec: cacheExpireSec,
ResourcePath: resourcePath,
}
}

View File

@@ -0,0 +1,33 @@
package constant
const (
// CodeKeyPrefix 缓存key前缀
CodeKeyPrefix = "RUNNING:CAPTCHA:%s"
// BlockPuzzleCaptcha 滑动验证码服务标识
BlockPuzzleCaptcha = "blockPuzzle"
// ClickWordCaptcha 点击验证码服务标识
ClickWordCaptcha = "clickWord"
// MemCacheKey 内存缓存标识
MemCacheKey = "mem"
// RedisCacheKey redis缓存标识
RedisCacheKey = "redis"
// DefaultFont 字体文件地址
DefaultFont = "/resources/fonts/WenQuanZhengHei.ttf"
// DefaultResourceRoot 默认根目录
DefaultResourceRoot = "./"
// DefaultText 默认水印显示文字
DefaultText = "我的水印"
)
const (
// DefaultTemplateImageDirectory 滑动模板图文件目录地址
DefaultTemplateImageDirectory = "/resources/defaultImages/jigsaw/slidingBlock"
// DefaultBackgroundImageDirectory 背景图片目录地址
DefaultBackgroundImageDirectory = "/resources/defaultImages/jigsaw/original"
// DefaultClickBackgroundImageDirectory 点击背景图默认地址
DefaultClickBackgroundImageDirectory = "/resources/defaultImages/pic-click"
)

View File

@@ -0,0 +1,37 @@
package vo
import (
"encoding/json"
"math"
)
type PointVO struct {
X int `json:"x,float64"`
Y int `json:"y,float64"`
SecretKey string
}
func (p *PointVO) SetSecretKey(secretKey string) {
p.SecretKey = secretKey
}
func NewPointVO(x int, y int) *PointVO {
return &PointVO{X: x, Y: y}
}
func (p *PointVO) UnmarshalJSON(data []byte) error {
clientPoint := struct {
X float64 `json:"x,float64"`
Y float64 `json:"y,float64"`
SecretKey string
}{}
if err := json.Unmarshal(data, &clientPoint); err != nil {
return err
}
p.Y = int(math.Floor(clientPoint.Y))
p.X = int(math.Floor(clientPoint.X))
p.SecretKey = clientPoint.SecretKey
return nil
}

View File

@@ -0,0 +1,233 @@
package service
import (
"encoding/json"
"errors"
"fmt"
"log"
"math"
constant "x_admin/util/aj-captcha-go/const"
"x_admin/util/aj-captcha-go/model/vo"
"x_admin/util/aj-captcha-go/util"
img "x_admin/util/aj-captcha-go/util/image"
"golang.org/x/image/colornames"
)
type BlockPuzzleCaptchaService struct {
point vo.PointVO
factory *CaptchaServiceFactory
}
func NewBlockPuzzleCaptchaService(factory *CaptchaServiceFactory) *BlockPuzzleCaptchaService {
// 初始化静态资源
img.SetUp(factory.config.ResourcePath)
return &BlockPuzzleCaptchaService{
factory: factory,
}
}
// Get 获取验证码图片信息
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)
// 初始化模板图片
templateImage := img.GetTemplateImage()
// 构造前端所需图片
b.pictureTemplatesCut(backgroundImage, templateImage)
originalImageBase64, err := backgroundImage.Base64()
jigsawImageBase64, err := templateImage.Base64()
if err != nil {
return nil, err
}
data := make(map[string]interface{})
data["originalImageBase64"] = originalImageBase64
data["jigsawImageBase64"] = jigsawImageBase64
data["secretKey"] = b.point.SecretKey
data["token"] = util.GetUuid()
codeKey := fmt.Sprintf(constant.CodeKeyPrefix, data["token"])
jsonPoint, err := json.Marshal(b.point)
if err != nil {
log.Printf("point json Marshal err: %v", err)
return nil, err
}
b.factory.GetCache().Set(codeKey, string(jsonPoint), b.factory.config.CacheExpireSec)
return data, nil
}
func (b *BlockPuzzleCaptchaService) pictureTemplatesCut(backgroundImage *util.ImageUtil, templateImage *util.ImageUtil) {
// 生成拼图坐标点
b.generateJigsawPoint(backgroundImage, templateImage)
// 裁剪模板图
b.cutByTemplate(backgroundImage, templateImage, b.point.X, 0)
// 插入干扰图
for {
newTemplateImage := img.GetTemplateImage()
if newTemplateImage.Src != templateImage.Src {
offsetX := util.RandomInt(0, backgroundImage.Width-newTemplateImage.Width-5)
if math.Abs(float64(newTemplateImage.Width-offsetX)) > float64(newTemplateImage.Width/2) {
b.interferenceByTemplate(backgroundImage, newTemplateImage, offsetX, b.point.Y)
break
}
}
}
}
// 插入干扰图
func (b *BlockPuzzleCaptchaService) interferenceByTemplate(backgroundImage *util.ImageUtil, templateImage *util.ImageUtil, x1 int, y1 int) {
xLength := templateImage.Width
yLength := templateImage.Height
for x := 0; x < xLength; x++ {
for y := 0; y < yLength; y++ {
// 如果模板图像当前像素点不是透明色 copy源文件信息到目标图片中
isOpacity := templateImage.IsOpacity(x, y)
// 当前模板像素在背景图中的位置
backgroundX := x + x1
backgroundY := y + y1
// 当不为透明时
if !isOpacity {
// 背景图区域模糊
backgroundImage.VagueImage(backgroundX, backgroundY)
}
//防止数组越界判断
if x == (xLength-1) || y == (yLength-1) {
continue
}
rightOpacity := templateImage.IsOpacity(x+1, y)
downOpacity := templateImage.IsOpacity(x, y+1)
//描边处理,,取带像素和无像素的界点,判断该点是不是临界轮廓点,如果是设置该坐标像素是白色
if (isOpacity && !rightOpacity) || (!isOpacity && rightOpacity) || (isOpacity && !downOpacity) || (!isOpacity && downOpacity) {
backgroundImage.RgbaImage.SetRGBA(backgroundX, backgroundY, colornames.White)
}
}
}
}
func (b *BlockPuzzleCaptchaService) cutByTemplate(backgroundImage *util.ImageUtil, templateImage *util.ImageUtil, x1, y1 int) {
xLength := templateImage.Width
yLength := templateImage.Height
for x := 0; x < xLength; x++ {
for y := 0; y < yLength; y++ {
// 如果模板图像当前像素点不是透明色 copy源文件信息到目标图片中
isOpacity := templateImage.IsOpacity(x, y)
// 当前模板像素在背景图中的位置
backgroundX := x + x1
backgroundY := y + y1
// 当不为透明时
if !isOpacity {
// 获取原图像素
backgroundRgba := backgroundImage.RgbaImage.RGBAAt(backgroundX, backgroundY)
// 将原图的像素扣到模板图上
templateImage.SetPixel(backgroundRgba, x, y)
// 背景图区域模糊
backgroundImage.VagueImage(backgroundX, backgroundY)
}
//防止数组越界判断
if x == (xLength-1) || y == (yLength-1) {
continue
}
rightOpacity := templateImage.IsOpacity(x+1, y)
downOpacity := templateImage.IsOpacity(x, y+1)
//描边处理,,取带像素和无像素的界点,判断该点是不是临界轮廓点,如果是设置该坐标像素是白色
if (isOpacity && !rightOpacity) || (!isOpacity && rightOpacity) || (isOpacity && !downOpacity) || (!isOpacity && downOpacity) {
templateImage.RgbaImage.SetRGBA(x, y, colornames.White)
backgroundImage.RgbaImage.SetRGBA(backgroundX, backgroundY, colornames.White)
}
}
}
}
// 生成模板图在背景图中的随机坐标点
func (b *BlockPuzzleCaptchaService) generateJigsawPoint(backgroundImage *util.ImageUtil, templateImage *util.ImageUtil) {
widthDifference := backgroundImage.Width - templateImage.Width
heightDifference := backgroundImage.Height - templateImage.Height
x, y := 0, 0
if widthDifference <= 0 {
x = 5
} else {
x = util.RandomInt(100, widthDifference-100)
}
if heightDifference <= 0 {
y = 5
} else {
y = util.RandomInt(5, heightDifference)
}
point := vo.PointVO{X: x, Y: y}
point.SetSecretKey(util.RandString(16))
b.point = point
}
func (b *BlockPuzzleCaptchaService) Check(token string, pointJson string) error {
cache := b.factory.GetCache()
codeKey := fmt.Sprintf(constant.CodeKeyPrefix, token)
cachePointInfo := cache.Get(codeKey)
if cachePointInfo == "" {
return errors.New("验证码已失效")
}
// 解析结构体
cachePoint := &vo.PointVO{}
userPoint := &vo.PointVO{}
err := json.Unmarshal([]byte(cachePointInfo), cachePoint)
if err != nil {
return err
}
// 解密前端传递过来的数据
userPointJson := util.AesDecrypt(pointJson, cachePoint.SecretKey)
err = json.Unmarshal([]byte(userPointJson), userPoint)
if err != nil {
return err
}
// 校验两个点是否符合
if math.Abs(float64(cachePoint.X-userPoint.X)) <= float64(b.factory.config.BlockPuzzle.Offset) && cachePoint.Y == userPoint.Y {
return nil
}
return errors.New("验证失败")
}
func (b *BlockPuzzleCaptchaService) Verification(token string, pointJson string) error {
err := b.Check(token, pointJson)
if err != nil {
return err
}
codeKey := fmt.Sprintf(constant.CodeKeyPrefix, token)
b.factory.GetCache().Delete(codeKey)
return nil
}

View File

@@ -0,0 +1,10 @@
package service
type CaptchaCacheInterface interface {
Get(key string) string
Set(key string, val string, expiresInSeconds int)
Delete(key string)
Exists(key string) bool
GetType() string
Increment(key string, val int) int
}

View File

@@ -0,0 +1,13 @@
package service
type CaptchaInterface interface {
// Get 获取验证码
Get() (map[string]interface{}, error)
// Check 核对验证码
Check(token string, pointJson string) error
// Verification 二次校验验证码(后端)
Verification(token string, pointJson string) error
}

View File

@@ -0,0 +1,58 @@
package service
import (
"log"
"sync"
configIns "x_admin/util/aj-captcha-go/config"
)
// CaptchaServiceFactory 验证码服务工厂
type CaptchaServiceFactory struct {
config *configIns.Config
ServiceMap map[string]CaptchaInterface
ServiceLock sync.RWMutex
CacheMap map[string]CaptchaCacheInterface
CacheLock sync.RWMutex
}
func NewCaptchaServiceFactory(config *configIns.Config) *CaptchaServiceFactory {
factory := &CaptchaServiceFactory{
ServiceMap: make(map[string]CaptchaInterface),
CacheMap: make(map[string]CaptchaCacheInterface),
config: config,
}
return factory
}
func (c *CaptchaServiceFactory) GetCache() CaptchaCacheInterface {
key := c.config.CacheType
c.CacheLock.RLock()
defer c.CacheLock.RUnlock()
if _, ok := c.CacheMap[key]; !ok {
log.Printf("未注册%s类型的Cache", key)
}
return c.CacheMap[key]
}
func (c *CaptchaServiceFactory) RegisterCache(key string, cacheInterface CaptchaCacheInterface) {
c.CacheLock.Lock()
defer c.CacheLock.Unlock()
c.CacheMap[key] = cacheInterface
}
func (c *CaptchaServiceFactory) RegisterService(key string, service CaptchaInterface) {
c.ServiceLock.Lock()
defer c.ServiceLock.Unlock()
c.ServiceMap[key] = service
}
func (c *CaptchaServiceFactory) GetService(key string) CaptchaInterface {
c.ServiceLock.RLock()
defer c.ServiceLock.RUnlock()
if _, ok := c.ServiceMap[key]; !ok {
log.Printf("未注册%s类型的Service", key)
}
return c.ServiceMap[key]
}

View File

@@ -0,0 +1,181 @@
package service
import (
"encoding/json"
"errors"
"fmt"
"log"
constant "x_admin/util/aj-captcha-go/const"
"x_admin/util/aj-captcha-go/model/vo"
"x_admin/util/aj-captcha-go/util"
img "x_admin/util/aj-captcha-go/util/image"
)
const (
TEXT = "的一了是我不在人们有来他这上着个地到大里说就去子得也和那要下看天时过出小么起你都把好还多没为又可家学只以主会样年想生同老中十从自面前头道它后然走很像见两用她国动进成回什边作对开而己些现山民候经发工向事命给长水几义三声于高手知理眼志点心战二问但身方实吃做叫当住听革打呢真全才四已所敌之最光产情路分总条白话东席次亲如被花口放儿常气五第使写军吧文运再果怎定许快明行因别飞外树物活部门无往船望新带队先力完却站代员机更九您每风级跟笑啊孩万少直意夜比阶连车重便斗马哪化太指变社似士者干石满日决百原拿群究各六本思解立河村八难早论吗根共让相研今其书坐接应关信觉步反处记将千找争领或师结块跑谁草越字加脚紧爱等习阵怕月青半火法题建赶位唱海七女任件感准张团屋离色脸片科倒睛利世刚且由送切星导晚表够整认响雪流未场该并底深刻平伟忙提确近亮轻讲农古黑告界拉名呀土清阳照办史改历转画造嘴此治北必服雨穿内识验传业菜爬睡兴形量咱观苦体众通冲合破友度术饭公旁房极南枪读沙岁线野坚空收算至政城劳落钱特围弟胜教热展包歌类渐强数乡呼性音答哥际旧神座章帮啦受系令跳非何牛取入岸敢掉忽种装顶急林停息句区衣般报叶压慢叔背细"
)
type ClickWordCaptchaService struct {
factory *CaptchaServiceFactory
}
func NewClickWordCaptchaService(factory *CaptchaServiceFactory) *ClickWordCaptchaService {
img.SetUp(factory.config.ResourcePath)
return &ClickWordCaptchaService{factory: factory}
}
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)
cachePointInfo := cache.Get(codeKey)
if cachePointInfo == "" {
return errors.New("验证码已失效")
}
// 解析结构体
var cachePoint []vo.PointVO
var userPoint []vo.PointVO
err := json.Unmarshal([]byte(cachePointInfo), &cachePoint)
if err != nil {
return err
}
// 解密前端传递过来的数据
userPointJson := util.AesDecrypt(pointJson, cachePoint[0].SecretKey)
err = json.Unmarshal([]byte(userPointJson), &userPoint)
if err != nil {
return err
}
for i, pointVO := range cachePoint {
targetPoint := userPoint[i]
fontsize := c.factory.config.ClickWord.FontSize
if targetPoint.X-fontsize > pointVO.X || targetPoint.X > pointVO.X+fontsize || targetPoint.Y-fontsize > pointVO.Y || targetPoint.Y > pointVO.Y+fontsize {
return errors.New("验证失败")
}
}
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)
c.factory.GetCache().Delete(codeKey)
return nil
}
func (c *ClickWordCaptchaService) getImageData(image *util.ImageUtil) ([]vo.PointVO, []string, error) {
wordCount := c.factory.config.ClickWord.FontNum
// 某个字不参与校验
num := util.RandomInt(1, wordCount)
currentWord := c.getRandomWords(wordCount)
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)
point.SetSecretKey(key)
// 随机设置文字 TODO 角度未设置
err := image.SetArtText(s, c.factory.config.ClickWord.FontSize, point)
if err != nil {
return nil, nil, err
}
if (num - 1) != i {
pointList = append(pointList, point)
wordList = append(wordList, s)
}
i++
}
return pointList, wordList, nil
}
// getRandomWords 获取随机文件
func (c *ClickWordCaptchaService) getRandomWords(count int) []string {
runesArray := []rune(TEXT)
size := len(runesArray)
set := make(map[string]bool)
var wordList []string
for {
word := runesArray[util.RandomInt(0, size-1)]
set[string(word)] = true
if len(set) >= count {
for str, _ := range set {
wordList = append(wordList, str)
}
break
}
}
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
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)
return vo.PointVO{X: x, Y: y}
}

View File

@@ -0,0 +1,48 @@
package service
import (
"strconv"
"x_admin/util/aj-captcha-go/util"
)
type MemCacheService struct {
Cache *util.CacheUtil
}
func NewMemCacheService(captchaCacheMaxNumber int) CaptchaCacheInterface {
return &MemCacheService{Cache: util.NewCacheUtil(captchaCacheMaxNumber)}
}
func (l *MemCacheService) Get(key string) string {
return l.Cache.Get(key)
}
func (l *MemCacheService) Set(key string, val string, expiresInSeconds int) {
l.Cache.Set(key, val, expiresInSeconds)
}
func (l *MemCacheService) Delete(key string) {
l.Cache.Delete(key)
}
func (l *MemCacheService) Exists(key string) bool {
return l.Cache.Exists(key)
}
func (l *MemCacheService) GetType() string {
return "mem"
}
func (l *MemCacheService) Increment(key string, val int) int {
cacheVal := l.Cache.Get(key)
num, err := strconv.Atoi(cacheVal)
if err != nil {
num = 0
}
ret := num + val
l.Cache.Set(key, strconv.Itoa(ret), 0)
return ret
}

View File

@@ -0,0 +1,49 @@
package service
import (
"strconv"
"x_admin/util/aj-captcha-go/util"
)
type RedisCacheService struct {
Cache *util.RedisUtil
}
// NewConfigRedisCacheService 初始化自定义redis配置
func NewConfigRedisCacheService(rdsAddr []string, dbUserName, dbPassword string, enableCluster bool, db int) CaptchaCacheInterface {
redisUtils := util.NewConfigRedisUtil(rdsAddr, dbUserName, dbPassword, enableCluster, db)
return &RedisCacheService{Cache: redisUtils}
}
func (l *RedisCacheService) Get(key string) string {
return l.Cache.Get(key)
}
func (l *RedisCacheService) Set(key string, val string, expiresInSeconds int) {
l.Cache.Set(key, val, expiresInSeconds)
}
func (l *RedisCacheService) Delete(key string) {
l.Cache.Delete(key)
}
func (l *RedisCacheService) Exists(key string) bool {
return l.Cache.Exists(key)
}
func (l *RedisCacheService) GetType() string {
return "redis"
}
func (l *RedisCacheService) Increment(key string, val int) int {
cacheVal := l.Cache.Get(key)
num, err := strconv.Atoi(cacheVal)
if err != nil {
num = 0
}
ret := num + val
l.Cache.Set(key, strconv.Itoa(ret), 0)
return ret
}

View File

@@ -0,0 +1,64 @@
package util
import (
"bytes"
"crypto/aes"
"encoding/base64"
)
func AesEncrypt(data, key string) string {
if key == "" {
return data
}
return base64.StdEncoding.EncodeToString(AesEncryptToBytes([]byte(data), []byte(key)))
}
func AesEncryptToBytes(data, key []byte) []byte {
block, _ := aes.NewCipher(key)
data = PKCS5Padding(data, block.BlockSize())
decrypted := make([]byte, len(data))
size := block.BlockSize()
for bs, be := 0, size; bs < len(data); bs, be = bs+size, be+size {
block.Encrypt(decrypted[bs:be], data[bs:be])
}
return decrypted
}
func AesDecrypt(point string, key string) string {
encryptBytes, _ := base64.StdEncoding.DecodeString(point)
info := AESDecryptECB(encryptBytes, []byte(key))
return string(info)
}
func AESDecryptECB(data, key []byte) []byte {
block, _ := aes.NewCipher(key)
decrypted := make([]byte, len(data))
size := block.BlockSize()
for bs, be := 0, size; bs < len(data); bs, be = bs+size, be+size {
block.Decrypt(decrypted[bs:be], data[bs:be])
}
return PKCS5UnPadding(decrypted)
}
// PKCS5UnPadding 删除pks5填充的尾部数据
func PKCS5UnPadding(origData []byte) []byte {
// 1. 计算数据的总长度
length := len(origData)
if length == 0 {
return origData
}
// 2. 根据填充的字节值得到填充的次数
number := int(origData[length-1])
// 3. 将尾部填充的number个字节去掉
return origData[:(length - number)]
}
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}

View File

@@ -0,0 +1,89 @@
package util
import (
"log"
"strconv"
"sync"
"time"
)
type CacheUtil struct {
Data map[string]string
DataRWLock sync.RWMutex
CaptchaCacheMaxNumber int
}
func NewCacheUtil(captchaCacheMaxNumber int) *CacheUtil {
return &CacheUtil{
Data: make(map[string]string),
CaptchaCacheMaxNumber: captchaCacheMaxNumber,
}
}
func (l *CacheUtil) Exists(key string) bool {
l.DataRWLock.RLock()
timeVal := l.Data[key+"_HoldTime"]
cacheHoldTime, err := strconv.ParseInt(timeVal, 10, 64)
l.DataRWLock.RUnlock()
if err != nil {
return false
}
if cacheHoldTime == 0 {
return true
}
if cacheHoldTime < time.Now().Unix() {
l.Delete(key)
return false
}
return true
}
func (l *CacheUtil) Get(key string) string {
if l.Exists(key) {
l.DataRWLock.RLock()
val := l.Data[key]
l.DataRWLock.RUnlock()
return val
}
return ""
}
func (l *CacheUtil) Set(key string, val string, expiresInSeconds int) {
//设置阈值达到即clear缓存
if len(l.Data) >= l.CaptchaCacheMaxNumber*2 {
log.Println("CACHE_MAP达到阈值clear map")
l.Clear()
}
l.DataRWLock.Lock()
l.Data[key] = val
if expiresInSeconds > 0 {
// 缓存失效时间
nowTime := time.Now().Unix() + int64(expiresInSeconds)
l.Data[key+"_HoldTime"] = strconv.FormatInt(nowTime, 10)
} else {
l.Data[key+"_HoldTime"] = strconv.FormatInt(0, 10)
}
l.DataRWLock.Unlock()
}
func (l *CacheUtil) Delete(key string) {
l.DataRWLock.Lock()
defer l.DataRWLock.Unlock()
delete(l.Data, key)
delete(l.Data, key+"_HoldTime")
}
func (l *CacheUtil) Clear() {
for key, _ := range l.Data {
l.Delete(key)
}
}

View File

@@ -0,0 +1,51 @@
package util
import (
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
"io/ioutil"
"log"
"unicode"
)
type FontUtil struct {
Src string
}
// NewFontUtil 字体绝对路径
func NewFontUtil(src string) *FontUtil {
return &FontUtil{Src: src}
}
// GetFont 获取一个字体对象
func (f *FontUtil) GetFont() *truetype.Font {
fontSourceBytes, err := ioutil.ReadFile(f.Src)
if err != nil {
log.Println("读取字体失败:", err)
}
trueTypeFont, err := freetype.ParseFont(fontSourceBytes)
if err != nil {
log.Println("解析字体失败:", err)
}
return trueTypeFont
}
func GetEnOrChLength(text string) int {
enCount, zhCount := 0, 0
for _, t := range text {
if unicode.Is(unicode.Han, t) {
zhCount++
} else {
enCount++
}
}
chOffset := (25/2)*zhCount + 5
enOffset := enCount * 8
return chOffset + enOffset
}

View File

@@ -0,0 +1,78 @@
package image
import (
"log"
"os"
"path/filepath"
constant "x_admin/util/aj-captcha-go/const"
"x_admin/util/aj-captcha-go/util"
)
var backgroundImageArr []string
var clickBackgroundImageArr []string
var templateImageArr []string
var resourceAbsPath string
func SetUp(resourcePath string) {
resourceAbsPath = resourcePath
root := resourcePath
//root := "/Users/skyline/go/src/aj-captcha-go"
backgroundImageRoot := root + constant.DefaultBackgroundImageDirectory
templateImageRoot := root + constant.DefaultTemplateImageDirectory
clickBackgroundImageRoot := root + constant.DefaultClickBackgroundImageDirectory
err := filepath.Walk(backgroundImageRoot, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
backgroundImageArr = append(backgroundImageArr, path)
return nil
})
err = filepath.Walk(templateImageRoot, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
templateImageArr = append(templateImageArr, path)
return nil
})
err = filepath.Walk(clickBackgroundImageRoot, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
clickBackgroundImageArr = append(clickBackgroundImageArr, path)
return nil
})
if err != nil {
log.Printf("初始化resource目录失败请检查该目录是否存在 err: %v", err)
}
}
func GetBackgroundImage() *util.ImageUtil {
max := len(backgroundImageArr) - 1
if max <= 0 {
max = 1
}
return util.NewImageUtil(backgroundImageArr[util.RandomInt(0, max)], resourceAbsPath+constant.DefaultFont)
}
func GetTemplateImage() *util.ImageUtil {
max := len(templateImageArr) - 1
if max <= 0 {
max = 1
}
return util.NewImageUtil(templateImageArr[util.RandomInt(0, max)], resourceAbsPath+constant.DefaultFont)
}
func GetClickBackgroundImage() *util.ImageUtil {
max := len(templateImageArr) - 1
if max <= 0 {
max = 1
}
return util.NewImageUtil(clickBackgroundImageArr[util.RandomInt(0, max)], resourceAbsPath+constant.DefaultFont)
}

View File

@@ -0,0 +1,264 @@
package util
import (
"bytes"
"encoding/base64"
"image"
"image/color"
"image/draw"
"image/png"
"log"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"x_admin/util/aj-captcha-go/model/vo"
"github.com/golang/freetype"
)
type ImageUtil struct {
Src string
SrcImage image.Image
RgbaImage *image.RGBA
FontPath string
Width int
Height int
}
// NewImageUtil src为绝对路径
func NewImageUtil(src string, fontPath string) *ImageUtil {
srcImage := OpenPngImage(src)
return &ImageUtil{Src: src,
SrcImage: srcImage,
RgbaImage: ImageToRGBA(srcImage),
Width: srcImage.Bounds().Dx(),
Height: srcImage.Bounds().Dy(),
FontPath: fontPath,
}
}
// IsOpacity 该像素是否透明
func (i *ImageUtil) IsOpacity(x, y int) bool {
A := i.RgbaImage.RGBAAt(x, y).A
if float32(A) <= 125 {
return true
}
return false
}
// DecodeImageToFile 将图片转换为新的文件 调试使用
func (i *ImageUtil) DecodeImageToFile() {
filename := "drawImg.png"
file, err := os.Create(filename)
if err != nil {
log.Printf("创建 %s 失败 %v", filename, err)
}
err = png.Encode(file, i.RgbaImage)
if err != nil {
log.Printf("png %s Encode 失败 %v", filename, err)
}
}
// SetText 为图片设置文字
func (i *ImageUtil) SetText(text string, fontsize int, color color.RGBA) {
x := float64(i.Width) - float64(GetEnOrChLength(text))
y := float64(i.Height) - (25 / 2) + 7
font := NewFontUtil(i.FontPath)
fc := freetype.NewContext()
// 设置屏幕每英寸的分辨率
//fc.SetDPI(72)
// 设置用于绘制文本的字体
fc.SetFont(font.GetFont())
// 以磅为单位设置字体大小
fc.SetFontSize(float64(fontsize))
// 设置剪裁矩形以进行绘制
fc.SetClip(i.RgbaImage.Bounds())
// 设置目标图像
fc.SetDst(i.RgbaImage)
// 设置绘制操作的源图像,通常为 image.Uniform
fc.SetSrc(image.NewUniform(color))
// 设置水印地址
pt := freetype.Pt(int(x), int(y))
// 根据 Pt 的坐标值绘制给定的文本内容
_, err := fc.DrawString(text, pt)
if err != nil {
log.Println("构造水印失败:", err)
}
}
// SetArtText 为图片设置文字
func (i *ImageUtil) SetArtText(text string, fontsize int, point vo.PointVO) error {
font := NewFontUtil(i.FontPath)
fc := freetype.NewContext()
// 设置屏幕每英寸的分辨率
//fc.SetDPI(72)
// 设置用于绘制文本的字体
fc.SetFont(font.GetFont())
// 以磅为单位设置字体大小
fc.SetFontSize(float64(fontsize))
// 设置剪裁矩形以进行绘制
fc.SetClip(i.RgbaImage.Bounds())
// 设置目标图像
fc.SetDst(i.RgbaImage)
// 设置绘制操作的源图像,通常为 image.Uniform
fc.SetSrc(image.NewUniform(color.RGBA{R: uint8(RandomInt(1, 200)), G: uint8(RandomInt(1, 200)), B: uint8(RandomInt(1, 200)), A: 255}))
// 设置水印地址
pt := freetype.Pt(point.X, point.Y)
// 根据 Pt 的坐标值绘制给定的文本内容
_, err := fc.DrawString(text, pt)
if err != nil {
log.Printf("构造水印失败 err: %v", err)
return err
}
return nil
}
// SetPixel 为像素设置颜色
func (i *ImageUtil) SetPixel(rgba color.RGBA, x, y int) {
i.RgbaImage.SetRGBA(x, y, rgba)
}
// Base64 为像素设置颜色
func (i *ImageUtil) Base64() (string, error) {
// 开辟一个新的空buff
var buf bytes.Buffer
// img写入到buff
err := png.Encode(&buf, i.RgbaImage)
if err != nil {
log.Printf("img写入buf失败 err: %v", err)
return "", err
}
//开辟存储空间
dist := make([]byte, buf.Cap()+buf.Len())
// buff转成base64
base64.StdEncoding.Encode(dist, buf.Bytes())
return string(dist), nil
}
// VagueImage 模糊区域
func (i *ImageUtil) VagueImage(x int, y int) {
var red uint32
var green uint32
var blue uint32
var alpha uint32
points := [8][2]int{{0, 1}, {0, -1}, {1, 0}, {-1, 0}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}}
for _, point := range points {
pointX := x + point[0]
pointY := y + point[1]
if pointX < 0 || pointX >= i.Width || pointY < 0 || pointY >= i.Height {
continue
}
r, g, b, a := i.RgbaImage.RGBAAt(pointX, pointY).RGBA()
red += r >> 8
green += g >> 8
blue += b >> 8
alpha += a >> 8
}
var avg uint32
avg = 8
rgba := color.RGBA{R: uint8(red / avg), G: uint8(green / avg), B: uint8(blue / avg), A: uint8(alpha / avg)}
i.RgbaImage.SetRGBA(x, y, rgba)
}
// OpenPngImage 打开png图片
func OpenPngImage(src string) image.Image {
ff, err := os.Open(src)
if err != nil {
log.Printf("打开 %s 图片失败: %v", src, err)
}
img, err := png.Decode(ff)
if err != nil {
log.Printf("png %s decode 失败: %v", src, err)
}
return img
}
// ImageToRGBA 图片转rgba
func ImageToRGBA(img image.Image) *image.RGBA {
// No conversion needed if image is an *image.RGBA.
if dst, ok := img.(*image.RGBA); ok {
return dst
}
// Use the image/draw package to convert to *image.RGBA.
b := img.Bounds()
dst := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
draw.Draw(dst, dst.Bounds(), img, b.Min, draw.Src)
return dst
}
// CurrentAbPath 获取项目根目录
func CurrentAbPath() (dir string) {
// 如果是go run则返回temp目录 go build 则返回当前目录
dir = getCurrentAbPathByExecutable()
tempDir := getTmpDir()
// 如果是临时目录执行 从Caller中获取
if strings.Contains(dir, tempDir) || tempDir == "." {
dir = getCurrentAbPathByCaller()
}
// 执行目录非util目录
if !strings.HasSuffix(dir, "util") {
dir += "/util"
}
return filepath.Dir(dir)
}
// 获取当前执行文件绝对路径
func getCurrentAbPathByExecutable() string {
exePath, err := os.Executable()
if err != nil {
log.Fatal(err)
}
res, _ := filepath.EvalSymlinks(filepath.Dir(exePath))
return res
}
// 获取当前执行文件绝对路径go run
func getCurrentAbPathByCaller() string {
var abPath string
_, filename, _, ok := runtime.Caller(0)
if ok {
abPath = path.Dir(filename)
}
return abPath
}
// 获取系统临时目录兼容go run
func getTmpDir() string {
dir := os.Getenv("TEMP")
if dir == "" {
dir = os.Getenv("TMP")
}
res, _ := filepath.EvalSymlinks(dir)
return res
}

View File

@@ -0,0 +1,14 @@
package util
import (
"math/rand"
"time"
)
func RandomInt(min, max int) int {
if min >= max || max == 0 {
return max
}
rand.Seed(time.Now().UnixNano())
return rand.Intn(max-min) + min
}

View File

@@ -0,0 +1,97 @@
package util
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
"strconv"
"time"
)
type RedisUtil struct {
Rdb redis.UniversalClient
}
// InitConfigRedis 初始化自定义配置redis客户端可单机 可集群)
func (l *RedisUtil) InitConfigRedis(rdsAddr []string, dbUserName, dbPassword string, enableCluster bool, db int) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if enableCluster {
l.Rdb = redis.NewClusterClient(&redis.ClusterOptions{
Addrs: rdsAddr,
Username: dbUserName,
Password: dbPassword,
PoolSize: 100,
})
_, err := l.Rdb.Ping(ctx).Result()
if err != nil {
panic(err.Error())
}
} else {
l.Rdb = redis.NewClient(&redis.Options{
Addr: rdsAddr[0],
Username: dbUserName,
Password: dbPassword, // no password set
DB: db, // use select DB
PoolSize: 100, // 连接池大小
})
_, err := l.Rdb.Ping(ctx).Result()
if err != nil {
panic(err.Error())
}
}
}
func NewConfigRedisUtil(rdsAddr []string, dbUserName, dbPassword string, enableCluster bool, db int) *RedisUtil {
redisUtil := &RedisUtil{}
redisUtil.InitConfigRedis(rdsAddr, dbUserName, dbPassword, enableCluster, db)
return redisUtil
}
func (l *RedisUtil) Exists(key string) bool {
timeVal := l.Rdb.Get(context.Background(), key+"_HoldTime").Val()
cacheHoldTime, err := strconv.ParseInt(timeVal, 10, 64)
if err != nil {
return false
}
if cacheHoldTime == 0 {
return true
}
if cacheHoldTime < time.Now().Unix() {
l.Delete(key)
return false
}
return true
}
func (l *RedisUtil) Get(key string) string {
val := l.Rdb.Get(context.Background(), key).Val()
return val
}
func (l *RedisUtil) Set(key string, val string, expiresInSeconds int) {
//设置阈值达到即clear缓存
rdsResult := l.Rdb.Set(context.Background(), key, val, time.Duration(expiresInSeconds)*time.Second)
fmt.Println("rdsResult: ", rdsResult.String(), "rdsErr: ", rdsResult.Err())
if expiresInSeconds > 0 {
// 缓存失效时间
nowTime := time.Now().Unix() + int64(expiresInSeconds)
l.Rdb.Set(context.Background(), key+"_HoldTime", strconv.FormatInt(nowTime, 10), time.Duration(expiresInSeconds)*time.Second)
} else {
l.Rdb.Set(context.Background(), key+"_HoldTime", strconv.FormatInt(0, 10), time.Duration(expiresInSeconds)*time.Second)
}
}
func (l *RedisUtil) Delete(key string) {
l.Rdb.Del(context.Background(), key)
l.Rdb.Del(context.Background(), key+"_HoldTime")
}
func (l *RedisUtil) Clear() {
//for key, _ := range l.Data {
// l.Delete(key)
//}
}

View File

@@ -0,0 +1,34 @@
package util
import (
"bytes"
"crypto/rand"
"fmt"
"io"
rand2 "math/rand"
"time"
)
// GetUuid 获取UUID
func GetUuid() string {
b := make([]byte, 16)
io.ReadFull(rand.Reader, b)
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}
func RandString(codeLen int) string {
// 1. 定义原始字符串
rawStr := "jkwangagDGFHGSERKILMJHSNOPQR546413890_"
// 2. 定义一个buf并且将buf交给bytes往buf中写数据
buf := make([]byte, 0, codeLen)
b := bytes.NewBuffer(buf)
// 随机从中获取
rand2.Seed(time.Now().UnixNano())
for rawStrLen := len(rawStr); codeLen > 0; codeLen-- {
randNum := rand2.Intn(rawStrLen)
b.WriteByte(rawStr[randNum])
}
return b.String()
}