kra/internal/service/system/captcha.go

164 lines
3.5 KiB
Go
Raw Permalink 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 system
import (
"sync"
"time"
"kra/internal/conf"
"github.com/go-kratos/kratos/v2/transport/http"
"github.com/mojocn/base64Captcha"
)
// CaptchaService 验证码服务
type CaptchaService struct {
keyLong int
imgWidth int
imgHeight int
openCaptcha int // 防爆次数0表示关闭
openCaptchaTimeOut int // 缓存超时时间(秒)
store base64Captcha.Store
blackCache *BlackCache
}
// CaptchaConfig 验证码配置(保留用于兼容)
type CaptchaConfig struct {
KeyLong int
ImgWidth int
ImgHeight int
OpenCaptcha int
OpenCaptchaTimeOut int
}
// BlackCache 黑名单缓存
type BlackCache struct {
mu sync.RWMutex
items map[string]*cacheItem
}
type cacheItem struct {
value int
expiration time.Time
}
// NewBlackCache 创建黑名单缓存
func NewBlackCache() *BlackCache {
bc := &BlackCache{
items: make(map[string]*cacheItem),
}
go bc.cleanup()
return bc
}
func (c *BlackCache) Set(key string, value int, duration time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = &cacheItem{
value: value,
expiration: time.Now().Add(duration),
}
}
func (c *BlackCache) Get(key string) (int, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, ok := c.items[key]
if !ok || time.Now().After(item.expiration) {
return 0, false
}
return item.value, true
}
func (c *BlackCache) cleanup() {
ticker := time.NewTicker(time.Minute)
for range ticker.C {
c.mu.Lock()
now := time.Now()
for k, v := range c.items {
if now.After(v.expiration) {
delete(c.items, k)
}
}
c.mu.Unlock()
}
}
// CaptchaResponse 验证码响应
type CaptchaResponse struct {
CaptchaId string `json:"captchaId"`
PicPath string `json:"picPath"`
CaptchaLength int `json:"captchaLength"`
OpenCaptcha bool `json:"openCaptcha"`
}
// NewCaptchaService 创建验证码服务
func NewCaptchaService(c *conf.Captcha) *CaptchaService {
return &CaptchaService{
keyLong: int(c.KeyLong),
imgWidth: int(c.ImgWidth),
imgHeight: int(c.ImgHeight),
openCaptcha: int(c.OpenCaptcha),
openCaptchaTimeOut: int(c.OpenCaptchaTimeout),
store: base64Captcha.DefaultMemStore,
blackCache: NewBlackCache(),
}
}
// RegisterRoutes 注册路由
func (s *CaptchaService) RegisterRoutes(rg *RouterGroup) {
// 验证码(公开路由,无需认证)
rg.Public.POST("/base/captcha", s.handleCaptcha)
}
func (s *CaptchaService) handleCaptcha(ctx http.Context) error {
// 获取客户端IP
clientIP := ctx.Request().RemoteAddr
// 判断验证码是否开启
openCaptcha := s.openCaptcha
openCaptchaTimeOut := s.openCaptchaTimeOut
v, ok := s.blackCache.Get(clientIP)
if !ok {
s.blackCache.Set(clientIP, 1, time.Second*time.Duration(openCaptchaTimeOut))
}
var oc bool
if openCaptcha == 0 || openCaptcha < v {
oc = true
}
// 生成验证码
driver := base64Captcha.NewDriverDigit(
s.imgHeight,
s.imgWidth,
s.keyLong,
0.7,
80,
)
cp := base64Captcha.NewCaptcha(driver, s.store)
id, b64s, _, err := cp.Generate()
if err != nil {
return ctx.Result(200, map[string]any{
"code": 7,
"msg": "验证码获取失败",
})
}
return ctx.Result(200, map[string]any{
"code": 0,
"msg": "验证码获取成功",
"data": CaptchaResponse{
CaptchaId: id,
PicPath: b64s,
CaptchaLength: s.keyLong,
OpenCaptcha: oc,
},
})
}
// Verify 验证验证码
func (s *CaptchaService) Verify(id, answer string) bool {
return s.store.Verify(id, answer, true)
}