Files
backend/internal/service/captcha_service.go
lan 29f0bad2bc
All checks were successful
Build / build (push) Successful in 2m17s
Build / build-docker (push) Successful in 57s
feat(yggdrasil): implement standard error responses and UUID format improvements
- Add YggdrasilErrorResponse struct and standard error codes for protocol compliance
- Change UUID storage from varchar(36) to varchar(32) for unsigned format
- Add utility functions: GenerateUUID, FormatUUIDToNoDash, RandomHex
- Support unsigned query parameter in GetProfileByUUID endpoint
- Improve refresh token response with available profiles list
- Fix key pair retrieval to use correct database column (rsa_private_key)
- Update UUID validator to accept both 32-char and 36-char formats
- Add SignStringWithProfileRSA method for profile-specific signing
- Fix profile assignment validation in refresh token flow
2026-02-23 13:26:53 +08:00

192 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"carrotskin/pkg/config"
"carrotskin/pkg/redis"
"carrotskin/pkg/utils"
"context"
"errors"
"fmt"
"log"
"time"
"github.com/wenlng/go-captcha-assets/resources/imagesv2"
"github.com/wenlng/go-captcha-assets/resources/tiles"
"github.com/wenlng/go-captcha/v2/slide"
"go.uber.org/zap"
)
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坐标
}
// 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) {
// 生成uuid作为验证码进程唯一标识
captchaID = utils.GenerateUUID()
if captchaID == "" {
err = errors.New("生成验证码唯一标识失败")
return
}
captData, err := slideTileCapt.Generate()
if err != nil {
err = fmt.Errorf("生成验证码失败: %w", err)
return
}
blockData := captData.GetData()
if blockData == nil {
err = errors.New("获取验证码数据失败")
return
}
block, _ := json.Marshal(blockData)
var blockMap map[string]interface{}
if err = json.Unmarshal(block, &blockMap); err != nil {
err = fmt.Errorf("反序列化为map失败: %w", err)
return
}
// 提取x和y并转换为int类型
tx, ok := blockMap["x"].(float64)
if !ok {
err = errors.New("无法将x转换为float64")
return
}
var x = int(tx)
ty, ok := blockMap["y"].(float64)
if !ok {
err = errors.New("无法将y转换为float64")
return
}
y = int(ty)
masterImg, err = captData.GetMasterImage().ToBase64()
if err != nil {
err = fmt.Errorf("主图转换为base64失败: %w", err)
return
}
tileImg, err = captData.GetTileImage().ToBase64()
if err != nil {
err = fmt.Errorf("滑块图转换为base64失败: %w", err)
return
}
redisData := RedisData{
Tx: x,
Ty: y,
}
redisDataJSON, _ := json.Marshal(redisData)
redisKey := redisKeyPrefix + captchaID
expireTime := 300 * time.Second
// 使用注入的Redis客户端
if err = s.redis.Set(ctx, redisKey, redisDataJSON, expireTime); err != nil {
err = fmt.Errorf("存储验证码到redis失败: %w", err)
return
}
// 返回时 y 需要减10
y = y - 10
return
}
// Verify 验证验证码
func (s *captchaService) Verify(ctx context.Context, dx int, captchaID string) (bool, error) {
// 测试环境下直接通过验证
cfg, err := config.GetConfig()
if err == nil && cfg.IsTestEnvironment() {
return true, nil
}
redisKey := redisKeyPrefix + captchaID
// 从Redis获取验证信息使用注入的客户端
dataJSON, err := s.redis.Get(ctx, redisKey)
if err != nil {
if s.redis.Nil(err) { // 使用封装客户端的Nil错误
return false, errors.New("验证码已过期或无效")
}
return false, fmt.Errorf("redis查询失败: %w", err)
}
var redisData RedisData
if err := json.Unmarshal([]byte(dataJSON), &redisData); err != nil {
return false, fmt.Errorf("解析redis数据失败: %w", err)
}
tx := redisData.Tx
ty := redisData.Ty
ok := slide.Validate(dx, ty, tx, ty, paddingValue)
// 验证后立即删除Redis记录防止重复使用
if ok {
if err := s.redis.Del(ctx, redisKey); err != nil {
// 记录警告但不影响验证结果
s.logger.Warn("删除验证码Redis记录失败", zap.Error(err))
}
}
return ok, nil
}