package system import ( "sync" "time" "kra/internal/biz/system" "kra/pkg/jwt" "kra/pkg/response" "kra/pkg/utils" "github.com/gin-gonic/gin" "github.com/mojocn/base64Captcha" "github.com/redis/go-redis/v9" ) type BaseApi struct{} // LoginRequest 登录请求 type LoginRequest struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` Captcha string `json:"captcha"` CaptchaId string `json:"captchaId"` } // LoginResponse 登录响应 type LoginResponse struct { User interface{} `json:"user"` Token string `json:"token"` ExpiresAt int64 `json:"expiresAt"` } // JWT实例(需要在初始化时设置) var jwtInstance *jwt.JWT // 验证码存储 var captchaStore base64Captcha.Store = base64Captcha.DefaultMemStore // 验证码配置 var ( openCaptcha int = 0 // 防爆次数,0表示关闭 openCaptchaTimeOut int = 300 // 缓存超时时间(秒) useMultipoint bool // 是否开启多点登录 ) // 黑名单缓存 var blackCache = NewLoginBlackCache() // LoginBlackCache 登录黑名单缓存 type LoginBlackCache struct { mu sync.RWMutex items map[string]*loginCacheItem } type loginCacheItem struct { value int expiration time.Time } // NewLoginBlackCache 创建登录黑名单缓存 func NewLoginBlackCache() *LoginBlackCache { bc := &LoginBlackCache{ items: make(map[string]*loginCacheItem), } go bc.cleanup() return bc } func (c *LoginBlackCache) Set(key string, value int, duration time.Duration) { c.mu.Lock() defer c.mu.Unlock() c.items[key] = &loginCacheItem{ value: value, expiration: time.Now().Add(duration), } } func (c *LoginBlackCache) 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 *LoginBlackCache) Increment(key string, delta int) { c.mu.Lock() defer c.mu.Unlock() if item, ok := c.items[key]; ok { item.value += delta } } func (c *LoginBlackCache) 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() } } // SetJWTInstance 设置JWT实例 func SetJWTInstance(j *jwt.JWT) { jwtInstance = j } // SetCaptchaStore 设置验证码存储 func SetCaptchaStore(store base64Captcha.Store) { captchaStore = store } // SetCaptchaConfig 设置验证码配置 func SetCaptchaConfig(open, timeout int) { openCaptcha = open openCaptchaTimeOut = timeout } // SetUseMultipoint 设置是否开启多点登录 func SetUseMultipoint(use bool) { useMultipoint = use } // Login // @Tags Base // @Summary 用户登录 // @Produce application/json // @Param data body LoginRequest true "用户名, 密码, 验证码" // @Success 200 {object} response.Response{data=LoginResponse,msg=string} "返回包括用户信息,token,过期时间" // @Router /base/login [post] func (b *BaseApi) Login(c *gin.Context) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { response.FailWithMessage(err.Error(), c) return } key := c.ClientIP() // 判断验证码是否开启 v, ok := blackCache.Get(key) if !ok { blackCache.Set(key, 1, time.Second*time.Duration(openCaptchaTimeOut)) } // 检查是否需要验证码 var needCaptcha bool = openCaptcha == 0 || openCaptcha < v if needCaptcha && (req.Captcha == "" || req.CaptchaId == "" || !captchaStore.Verify(req.CaptchaId, req.Captcha, true)) { // 验证码次数+1 blackCache.Increment(key, 1) response.FailWithMessage("验证码错误", c) return } user, err := userUsecase.Login(c, req.Username, req.Password) if err != nil { // 验证码次数+1 blackCache.Increment(key, 1) response.FailWithMessage("用户名不存在或者密码错误", c) return } if user.Enable != 1 { // 验证码次数+1 blackCache.Increment(key, 1) response.FailWithMessage("用户被禁止登录", c) return } // 检查用户角色默认路由 if user.Authority != nil { defaultRouter := menuUsecase.UserAuthorityDefaultRouter(c, user.AuthorityId, user.Authority.DefaultRouter) user.Authority.DefaultRouter = defaultRouter } b.TokenNext(c, user) } // TokenNext 登录以后签发jwt func (b *BaseApi) TokenNext(c *gin.Context, user *system.User) { // 生成token claims := jwtInstance.CreateClaims(jwt.BaseClaims{ UUID: user.UUID.String(), ID: user.ID, Username: user.Username, NickName: user.NickName, AuthorityID: user.AuthorityId, }) token, err := jwtInstance.CreateToken(claims.BaseClaims) if err != nil { response.FailWithMessage("获取token失败", c) return } if !useMultipoint { utils.SetToken(c, token, int(claims.ExpiresAt.Unix()-time.Now().Unix())) response.OkWithDetailed(LoginResponse{ User: toUserResponse(user), Token: token, ExpiresAt: claims.ExpiresAt.UnixMilli(), }, "登录成功", c) return } // 多点登录处理 if jwtStr, err := jwtBlacklistUsecase.GetRedisJWT(c, user.Username); err == redis.Nil { // 没有旧token,直接设置新token if err := utils.SetRedisJWT(token, user.Username); err != nil { response.FailWithMessage("设置登录状态失败", c) return } utils.SetToken(c, token, int(claims.ExpiresAt.Unix()-time.Now().Unix())) response.OkWithDetailed(LoginResponse{ User: toUserResponse(user), Token: token, ExpiresAt: claims.ExpiresAt.UnixMilli(), }, "登录成功", c) } else if err != nil { response.FailWithMessage("设置登录状态失败", c) } else { // 有旧token,将旧token加入黑名单 if err := jwtBlacklistUsecase.JsonInBlacklist(c, jwtStr); err != nil { response.FailWithMessage("jwt作废失败", c) return } if err := utils.SetRedisJWT(token, user.Username); err != nil { response.FailWithMessage("设置登录状态失败", c) return } utils.SetToken(c, token, int(claims.ExpiresAt.Unix()-time.Now().Unix())) response.OkWithDetailed(LoginResponse{ User: toUserResponse(user), Token: token, ExpiresAt: claims.ExpiresAt.UnixMilli(), }, "登录成功", c) } } // CaptchaResponse 验证码响应 type CaptchaResponse struct { CaptchaId string `json:"captchaId"` PicPath string `json:"picPath"` CaptchaLength int `json:"captchaLength"` OpenCaptcha bool `json:"openCaptcha"` } // 验证码图片配置 var ( captchaImgHeight int = 80 captchaImgWidth int = 240 captchaKeyLong int = 6 ) // SetCaptchaImgConfig 设置验证码图片配置 func SetCaptchaImgConfig(height, width, keyLong int) { captchaImgHeight = height captchaImgWidth = width captchaKeyLong = keyLong } // Captcha // @Tags Base // @Summary 生成验证码 // @Produce application/json // @Success 200 {object} response.Response{data=CaptchaResponse,msg=string} "生成验证码,返回包括随机数id,base64,验证码长度,是否开启验证码" // @Router /base/captcha [post] func (b *BaseApi) Captcha(c *gin.Context) { key := c.ClientIP() v, ok := blackCache.Get(key) if !ok { blackCache.Set(key, 1, time.Second*time.Duration(openCaptchaTimeOut)) } var oc bool if openCaptcha == 0 || openCaptcha < v { oc = true } // 生成数字验证码 driver := base64Captcha.NewDriverDigit(captchaImgHeight, captchaImgWidth, captchaKeyLong, 0.7, 80) cp := base64Captcha.NewCaptcha(driver, captchaStore) id, b64s, _, err := cp.Generate() if err != nil { response.FailWithMessage("验证码获取失败", c) return } response.OkWithDetailed(CaptchaResponse{ CaptchaId: id, PicPath: b64s, CaptchaLength: captchaKeyLong, OpenCaptcha: oc, }, "验证码获取成功", c) }