kra/internal/server/handler/system/sys_base.go

304 lines
7.7 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/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)
}