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