2025-11-28 23:30:49 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"carrotskin/pkg/config"
|
|
|
|
|
|
"carrotskin/pkg/redis"
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"log"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
|
"github.com/wenlng/go-captcha-assets/resources/imagesv2"
|
|
|
|
|
|
"github.com/wenlng/go-captcha-assets/resources/tiles"
|
|
|
|
|
|
"github.com/wenlng/go-captcha/v2/slide"
|
2025-12-02 22:52:33 +08:00
|
|
|
|
"go.uber.org/zap"
|
2025-11-28 23:30:49 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
|
slideTileCapt slide.Captcha
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 常量定义(业务相关配置,与Redis连接配置分离)
|
|
|
|
|
|
const (
|
|
|
|
|
|
redisKeyPrefix = "captcha:" // Redis键前缀(便于区分业务)
|
|
|
|
|
|
paddingValue = 3 // 验证允许的误差像素(±3px)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Init 验证码图初始化
|
|
|
|
|
|
func init() {
|
|
|
|
|
|
builder := slide.NewBuilder()
|
|
|
|
|
|
bgImage, err := imagesv2.GetImages()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Fatalln(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
// 滑块形状获取
|
|
|
|
|
|
graphs := getSlideTileGraphArr()
|
|
|
|
|
|
|
|
|
|
|
|
builder.SetResources(
|
|
|
|
|
|
slide.WithGraphImages(graphs),
|
|
|
|
|
|
slide.WithBackgrounds(bgImage),
|
|
|
|
|
|
)
|
|
|
|
|
|
slideTileCapt = builder.Make()
|
|
|
|
|
|
if slideTileCapt == nil {
|
|
|
|
|
|
log.Fatalln("验证码实例初始化失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// getSlideTileGraphArr 滑块选择
|
|
|
|
|
|
func getSlideTileGraphArr() []*slide.GraphImage {
|
|
|
|
|
|
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,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return newGraphs
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RedisData 存储到Redis的验证信息(仅包含校验必需字段)
|
|
|
|
|
|
type RedisData struct {
|
|
|
|
|
|
Tx int `json:"tx"` // 滑块目标X坐标
|
|
|
|
|
|
Ty int `json:"ty"` // 滑块目标Y坐标
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
// captchaService CaptchaService的实现
|
|
|
|
|
|
type captchaService struct {
|
|
|
|
|
|
redis *redis.Client
|
|
|
|
|
|
logger *zap.Logger
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewCaptchaService 创建CaptchaService实例
|
|
|
|
|
|
func NewCaptchaService(redisClient *redis.Client, logger *zap.Logger) CaptchaService {
|
|
|
|
|
|
return &captchaService{
|
|
|
|
|
|
redis: redisClient,
|
|
|
|
|
|
logger: logger,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Generate 生成验证码
|
|
|
|
|
|
func (s *captchaService) Generate(ctx context.Context) (masterImg, tileImg, captchaID string, y int, err error) {
|
2025-11-28 23:30:49 +08:00
|
|
|
|
// 生成uuid作为验证码进程唯一标识
|
2025-12-02 22:52:33 +08:00
|
|
|
|
captchaID = uuid.NewString()
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if captchaID == "" {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
err = errors.New("生成验证码唯一标识失败")
|
|
|
|
|
|
return
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
captData, err := slideTileCapt.Generate()
|
|
|
|
|
|
if err != nil {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
err = fmt.Errorf("生成验证码失败: %w", err)
|
|
|
|
|
|
return
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
blockData := captData.GetData()
|
|
|
|
|
|
if blockData == nil {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
err = errors.New("获取验证码数据失败")
|
|
|
|
|
|
return
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
block, _ := json.Marshal(blockData)
|
|
|
|
|
|
var blockMap map[string]interface{}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
if err = json.Unmarshal(block, &blockMap); err != nil {
|
|
|
|
|
|
err = fmt.Errorf("反序列化为map失败: %w", err)
|
|
|
|
|
|
return
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 提取x和y并转换为int类型
|
|
|
|
|
|
tx, ok := blockMap["x"].(float64)
|
|
|
|
|
|
if !ok {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
err = errors.New("无法将x转换为float64")
|
|
|
|
|
|
return
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
var x = int(tx)
|
|
|
|
|
|
ty, ok := blockMap["y"].(float64)
|
|
|
|
|
|
if !ok {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
err = errors.New("无法将y转换为float64")
|
|
|
|
|
|
return
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
2025-12-02 22:52:33 +08:00
|
|
|
|
y = int(ty)
|
|
|
|
|
|
|
|
|
|
|
|
masterImg, err = captData.GetMasterImage().ToBase64()
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
err = fmt.Errorf("主图转换为base64失败: %w", err)
|
|
|
|
|
|
return
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
2025-12-02 22:52:33 +08:00
|
|
|
|
tileImg, err = captData.GetTileImage().ToBase64()
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
err = fmt.Errorf("滑块图转换为base64失败: %w", err)
|
|
|
|
|
|
return
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
2025-12-02 22:52:33 +08:00
|
|
|
|
|
2025-11-28 23:30:49 +08:00
|
|
|
|
redisData := RedisData{
|
|
|
|
|
|
Tx: x,
|
|
|
|
|
|
Ty: y,
|
|
|
|
|
|
}
|
|
|
|
|
|
redisDataJSON, _ := json.Marshal(redisData)
|
|
|
|
|
|
redisKey := redisKeyPrefix + captchaID
|
|
|
|
|
|
expireTime := 300 * time.Second
|
|
|
|
|
|
|
|
|
|
|
|
// 使用注入的Redis客户端
|
2025-12-02 22:52:33 +08:00
|
|
|
|
if err = s.redis.Set(ctx, redisKey, redisDataJSON, expireTime); err != nil {
|
|
|
|
|
|
err = fmt.Errorf("存储验证码到redis失败: %w", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 返回时 y 需要减10
|
|
|
|
|
|
y = y - 10
|
|
|
|
|
|
return
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
// Verify 验证验证码
|
|
|
|
|
|
func (s *captchaService) Verify(ctx context.Context, dx int, captchaID string) (bool, error) {
|
2025-11-30 18:56:56 +08:00
|
|
|
|
// 测试环境下直接通过验证
|
|
|
|
|
|
cfg, err := config.GetConfig()
|
|
|
|
|
|
if err == nil && cfg.IsTestEnvironment() {
|
|
|
|
|
|
return true, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
redisKey := redisKeyPrefix + captchaID
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
|
|
|
|
|
// 从Redis获取验证信息,使用注入的客户端
|
2025-12-02 22:52:33 +08:00
|
|
|
|
dataJSON, err := s.redis.Get(ctx, redisKey)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
if s.redis.Nil(err) { // 使用封装客户端的Nil错误
|
2025-11-28 23:30:49 +08:00
|
|
|
|
return false, errors.New("验证码已过期或无效")
|
|
|
|
|
|
}
|
2025-11-30 18:56:56 +08:00
|
|
|
|
return false, fmt.Errorf("redis查询失败: %w", err)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
var redisData RedisData
|
|
|
|
|
|
if err := json.Unmarshal([]byte(dataJSON), &redisData); err != nil {
|
2025-11-30 18:56:56 +08:00
|
|
|
|
return false, fmt.Errorf("解析redis数据失败: %w", err)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
tx := redisData.Tx
|
|
|
|
|
|
ty := redisData.Ty
|
|
|
|
|
|
ok := slide.Validate(dx, ty, tx, ty, paddingValue)
|
|
|
|
|
|
|
|
|
|
|
|
// 验证后立即删除Redis记录(防止重复使用)
|
|
|
|
|
|
if ok {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
if err := s.redis.Del(ctx, redisKey); err != nil {
|
2025-11-28 23:30:49 +08:00
|
|
|
|
// 记录警告但不影响验证结果
|
2025-12-02 22:52:33 +08:00
|
|
|
|
s.logger.Warn("删除验证码Redis记录失败", zap.Error(err))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return ok, nil
|
|
|
|
|
|
}
|