diff --git a/app/admin/api/system/config.go b/app/admin/api/system/config.go index be2191afb064570d4b48ee92382792ae7eb05ee5..10c83bcebedb321cf3fa4860e7a5e264abdd521e 100644 --- a/app/admin/api/system/config.go +++ b/app/admin/api/system/config.go @@ -2,8 +2,10 @@ package system import ( "net/http" + "ruoyi-go/app/admin/model/cache" "ruoyi-go/app/admin/model/system" "ruoyi-go/app/admin/model/tools" + "ruoyi-go/utils" "ruoyi-go/utils/R" "strconv" "time" @@ -127,10 +129,13 @@ func GetConfigInfo(context *gin.Context) { func GetConfigKey(context *gin.Context) { configKey := context.Param("configKey") - var config = system.SysConfig{ConfigKey: configKey} - var result = system.SelectConfig(config) + value := cache.GetSysConfigCacheByKey(configKey) + if len(value) <= 0 { + var result = system.SelectConfig(system.SysConfig{ConfigKey: configKey}) + value = result.ConfigValue + } context.JSON(http.StatusOK, gin.H{ - "msg": result.ConfigValue, + "msg": value, "code": http.StatusOK, }) } @@ -146,6 +151,7 @@ func SaveConfig(context *gin.Context) { configParam.CreateBy = user.UserName configParam.CreateTime = time.Now() result := system.SaveConfig(configParam) + cache.RefreshSysConfigCache(configParam.ConfigKey) context.JSON(http.StatusOK, result) } @@ -160,6 +166,7 @@ func UploadConfig(context *gin.Context) { configParam.UpdateBy = user.UserName configParam.UpdateTime = time.Now() result := system.EditConfig(configParam) + cache.RefreshSysConfigCache(configParam.ConfigKey) context.JSON(http.StatusOK, result) } @@ -167,14 +174,42 @@ func DetectConfig(context *gin.Context) { userId, _ := context.Get("userId") println(userId) var configIds = context.Param("configIds") + var ids = utils.Split(configIds) + var configKeys []string + for i := 0; i < len(ids); i++ { + id := ids[i] + var config = system.GetConfigInfo(id) + if "Y" == config.ConfigType { + context.JSON(http.StatusOK, R.ReturnFailMsg("内置参数"+config.ConfigKey+"不能删除 ")) + return + } + configKeys = append(configKeys, config.ConfigKey) + } result := system.DelConfig(configIds) + for _, key := range configKeys { + cache.DeleteSysConfigCache(key) + } context.JSON(http.StatusOK, result) } -func DeleteCacheConfig(context *gin.Context) { - userId, _ := context.Get("userId") - println(userId) - var refreshCache = context.Param("refreshCache") - result := system.DelCacheConfig(refreshCache) - context.JSON(http.StatusOK, result) +func RefreshCacheConfig(context *gin.Context) { + err := cache.DeleteAllSysConfigCache() + if err != nil { + context.JSON(http.StatusOK, R.ReturnFailMsg(err.Error())) + return + } + cache.InitSysConfigCache() + context.JSON(http.StatusOK, R.ReturnSuccess("操作成功")) +} + +func SelectCaptchaEnabled() bool { + var configValue = cache.GetSysConfigCacheByKey("sys.account.captchaEnabled") + if len(configValue) <= 0 { + configValue = system.SelectConfigByKey("sys.account.captchaEnabled") + } + boolValue, err := strconv.ParseBool(configValue) + if err != nil { + return true + } + return boolValue } diff --git a/app/admin/api/system/user.go b/app/admin/api/system/user.go index 426438b637402b701be2d87a8cbcf3321c1ba2c3..8520e2a169cf23286f82f9825d94acaa7bd9ebac 100644 --- a/app/admin/api/system/user.go +++ b/app/admin/api/system/user.go @@ -7,6 +7,7 @@ import ( useragent "github.com/wenlng/go-user-agent" "github.com/xuri/excelize/v2" "net/http" + "ruoyi-go/app/admin/model/cache" "ruoyi-go/app/admin/model/constants" "ruoyi-go/app/admin/model/monitor" "ruoyi-go/app/admin/model/system" @@ -44,7 +45,8 @@ func LoginHandler(context *gin.Context) { findUser(param, context) } func loginBeforeCheck(param system.LoginParam, context *gin.Context) bool { - var captchaEnabled = system.SelectCaptchaEnabled() + // 验证码验证 + var captchaEnabled = SelectCaptchaEnabled() if !captchaEnabled { return false } @@ -64,68 +66,78 @@ func findUser(param system.LoginParam, context *gin.Context) { var loginName = param.UserName var pass = param.Password var user = system.FindUserByName(loginName) - if user.UserId != 0 { - if user.Status == "1" { - monitor.LoginInfoAdd(context, param, "登录失败,账号已停用", false) - context.JSON(http.StatusOK, R.ReturnFailMsg("账号已停用")) - return - } - // 验证 密码是否正确 - if utils.PasswordVerify(pass, user.Password) { - newUUID, _ := uuid.NewUUID() - tokenString, err := jwt.CreateToken(user.UserName, user.UserId, user.DeptId, newUUID.String()) - if err != nil { - monitor.LoginInfoAdd(context, param, "登录失败,"+err.Error(), false) - context.JSON(http.StatusOK, R.ReturnFailMsg("登录失败")) - return - } - dept := system.GetDeptInfo(strconv.Itoa(user.DeptId)) - userAgent := context.Request.Header.Get("User-Agent") - os := useragent.GetOsName(userAgent) - browser := useragent.GetBrowserName(userAgent) - tokenId := newUUID.String() - //写入缓存 - loginUser, _ := json.Marshal(&monitor.LoginUserCache{ - TokenId: tokenId, - DeptName: dept.DeptName, - Ipaddr: "" + context.ClientIP(), - LoginLocation: "" + utils.GetRealAddressByIP(context.ClientIP()), - UserId: user.UserId, - Browser: browser, - Os: os, - LoginTime: time.Now().Unix(), - UserName: user.UserName, - Uuid: tokenId, - DeptId: user.DeptId, - }) - err = redisCache.NewRedisCache().Put(constants.LoginCacheKey+tokenId, string(loginUser), time.Duration(config.Jwt.JwtTtl)*time.Second) - if err != nil { - monitor.LoginInfoAdd(context, param, "登录失败,"+err.Error(), false) - context.JSON(http.StatusOK, R.ReturnFailMsg("登录失败")) - return - } - // 登录日志 - monitor.LoginInfoAdd(context, param, "登录成功", true) - context.JSON(http.StatusOK, gin.H{ - "msg": "登录成功", - "code": http.StatusOK, - "token": tokenString, - }) - } else { - monitor.LoginInfoAdd(context, param, "登录失败,密码错误", false) - context.JSON(http.StatusOK, gin.H{ - "msg": "登录失败,密码错误", - "code": http.StatusInternalServerError, - }) - } - - } else { + if user.UserId <= 0 { monitor.LoginInfoAdd(context, param, "登录失败,用户不存在", false) context.JSON(http.StatusOK, gin.H{ "msg": "用户不存在", "code": http.StatusInternalServerError, }) + return + } + if user.Status == "1" { + monitor.LoginInfoAdd(context, param, "登录失败,账号已停用", false) + context.JSON(http.StatusOK, R.ReturnFailMsg("账号已停用")) + return + } + // 验证密码错误次数 + count := cache.GetPasswordTryCount(param.UserName) + if count >= config.UserPassword.MaxRetryCount { + monitor.LoginInfoAdd(context, param, "登录失败,用户密码错误最大次数", false) + context.JSON(http.StatusOK, R.ReturnFailMsg("登录失败,用户密码错误最大次数,请过稍后再试")) + return } + // 验证 密码是否正确 + if !utils.PasswordVerify(pass, user.Password) { + count++ + cache.SetPasswordTryCount(param.UserName, count) + monitor.LoginInfoAdd(context, param, "登录失败,密码错误", false) + context.JSON(http.StatusOK, gin.H{ + "msg": "登录失败,密码错误", + "code": http.StatusInternalServerError, + }) + return + } else { + cache.DeletePasswordTryCount(param.UserName) + } + newUUID, _ := uuid.NewUUID() + tokenId := newUUID.String() + dept := system.GetDeptInfo(strconv.Itoa(user.DeptId)) + userAgent := context.Request.Header.Get("User-Agent") + os := useragent.GetOsName(userAgent) + browser := useragent.GetBrowserName(userAgent) + tokenString, err := jwt.CreateToken(user.UserName, user.UserId, user.DeptId, newUUID.String()) + if err != nil { + monitor.LoginInfoAdd(context, param, "登录失败,"+err.Error(), false) + context.JSON(http.StatusOK, R.ReturnFailMsg("登录失败")) + return + } + //写入缓存 + loginUser, _ := json.Marshal(&monitor.LoginUserCache{ + TokenId: tokenId, + DeptName: dept.DeptName, + Ipaddr: "" + context.ClientIP(), + LoginLocation: "" + utils.GetRealAddressByIP(context.ClientIP()), + UserId: user.UserId, + Browser: browser, + Os: os, + LoginTime: time.Now().Unix(), + UserName: user.UserName, + Uuid: tokenId, + DeptId: user.DeptId, + }) + err = redisCache.NewRedisCache().Put(constants.LoginCacheKey+newUUID.String(), string(loginUser), time.Duration(config.Jwt.JwtTtl)*time.Second) + if err != nil { + monitor.LoginInfoAdd(context, param, "登录失败,"+err.Error(), false) + context.JSON(http.StatusOK, R.ReturnFailMsg("登录失败")) + return + } + // 登录日志 + monitor.LoginInfoAdd(context, param, "登录成功", true) + context.JSON(http.StatusOK, gin.H{ + "msg": "登录成功", + "code": http.StatusOK, + "token": tokenString, + }) } func GetInfoHandler(context *gin.Context) { @@ -164,11 +176,11 @@ func LogoutHandler(context *gin.Context) { // CaptchaImageHandler 验证码 输出 func CaptchaImageHandler(context *gin.Context) { - var captchaEnabled = system.SelectCaptchaEnabled() + var captchaEnabled = SelectCaptchaEnabled() if captchaEnabled { id, b64s, err := utils.CreateImageCaptcha() if err != nil { - context.JSON(http.StatusOK, R.ReturnFailMsg("创建二维码失败,请联系管理员")) + context.JSON(http.StatusOK, R.ReturnFailMsg("创建验证码失败,请联系管理员")) return } context.JSON(http.StatusOK, gin.H{ diff --git a/app/admin/model/cache/cache.go b/app/admin/model/cache/cache.go index cb388d750543e024d42f0a1813d9b76cfa7ef38a..18bd3bdb7a6eb8cce960ddf54245feb9b4ae4143 100644 --- a/app/admin/model/cache/cache.go +++ b/app/admin/model/cache/cache.go @@ -1,5 +1,8 @@ package cache func InitCache() { + //初始化字典 InitDictCache() + //初始化配置参数缓存 + InitSysConfigCache() } diff --git a/app/admin/model/cache/pwdErrCntCache.go b/app/admin/model/cache/pwdErrCntCache.go new file mode 100644 index 0000000000000000000000000000000000000000..830a1c496e7365762690a7de9b320972b4e71d2e --- /dev/null +++ b/app/admin/model/cache/pwdErrCntCache.go @@ -0,0 +1,32 @@ +package cache + +import ( + "ruoyi-go/app/admin/model/constants" + "ruoyi-go/config" + "ruoyi-go/pkg/cache/redisCache" + "strconv" + "time" +) + +// GetPasswordTryCount 获取输入次数 +func GetPasswordTryCount(username string) int { + countStr, err := redisCache.NewRedisCache().Get(constants.PwdErrCntCacheKey + username) + if err != nil || len(countStr) <= 0 { + return 0 + } + count, err := strconv.Atoi(countStr) + if err != nil { + return 0 + } + return count +} + +// SetPasswordTryCount 次数+1 +func SetPasswordTryCount(username string, count int) { + redisCache.NewRedisCache().Put(constants.PwdErrCntCacheKey+username, strconv.Itoa(count), time.Duration(config.UserPassword.LockTime)*time.Minute) +} + +// DeletePasswordTryCount 删除 +func DeletePasswordTryCount(username string) { + redisCache.NewRedisCache().Del(constants.PwdErrCntCacheKey + username) +} diff --git a/app/admin/model/cache/sysConfigCache.go b/app/admin/model/cache/sysConfigCache.go new file mode 100644 index 0000000000000000000000000000000000000000..9c907dcde2d818fd0e63eb2dc3ef9a940e2f89df --- /dev/null +++ b/app/admin/model/cache/sysConfigCache.go @@ -0,0 +1,56 @@ +package cache + +import ( + "ruoyi-go/app/admin/model/constants" + "ruoyi-go/app/admin/model/system" + "ruoyi-go/app/admin/model/tools" + "ruoyi-go/pkg/cache/redisCache" +) + +// InitSysConfigCache 初始化参数配置到redis +func InitSysConfigCache() { + var param = tools.SearchTableDataParam{ + Other: system.SysConfig{}, + } + result := system.SelectConfigList(param, false) + list := result.Rows.([]system.SysConfig) + for _, config := range list { + SetSysConfigCache(config.ConfigKey, config.ConfigValue) + } +} + +// RefreshSysConfigCache 刷新某个参数配置的所有值 +func RefreshSysConfigCache(configKey string) { + config := system.SelectConfigByKey(configKey) + SetSysConfigCache(configKey, config) +} + +// GetSysConfigCacheByKey 根据参数配置来获取字典数据 +func GetSysConfigCacheByKey(configKey string) string { + get, err := redisCache.NewRedisCache().Get(constants.SysConfigCacheKey + configKey) + if err != nil { + return "" + } + return get +} + +// SetSysConfigCache 设置参数配置 值 +func SetSysConfigCache(configKey string, configValue string) { + redisCache.NewRedisCache().Put(constants.SysConfigCacheKey+configKey, configValue, -1) +} + +// DeleteSysConfigCache 删除字典 +func DeleteSysConfigCache(configKey string) { + redisCache.NewRedisCache().Del(constants.SysConfigCacheKey + configKey) +} + +func DeleteAllSysConfigCache() error { + keys, _, err := redisCache.NewRedisCache().Scan(0, constants.SysConfigCacheKey+"*", constants.ScanCountMax) + if err != nil { + return err + } + for _, key := range keys { + redisCache.NewRedisCache().Del(key) + } + return nil +} diff --git a/app/admin/model/constants/sys_constants.go b/app/admin/model/constants/sys_constants.go index c13d6d11b2a7472bdbda0738924604c6b2a589c2..fafe1141fd8e2d62b75b082a9c1d085ca050e228 100644 --- a/app/admin/model/constants/sys_constants.go +++ b/app/admin/model/constants/sys_constants.go @@ -16,9 +16,12 @@ const ( ) const ( - // 防止和java冲突 - LoginCacheKey = "go_login_tokens:" - CaptchaCodesKey = "captcha_codes:" - SysDictCacheKey = "sys_dict:" - ScanCountMax = 1000 + LoginCacheKey = "go_login_tokens:" + CaptchaCodesKey = "captcha_codes:" + SysDictCacheKey = "sys_dict:" + SysConfigCacheKey = "sys_config:" + PwdErrCntCacheKey = "pwd_err_cnt:" + RepeatSubmitCacheKey = "repeat_submit:" + RateLimitCacheKey = "rate_limit:" + ScanCountMax = 1000 ) diff --git a/app/admin/model/system/sysConfig.go b/app/admin/model/system/sysConfig.go index 31fe131b445554b3357c41f1b74136e7ff46eb29..281d84531d169a6226e791e0c0d63efe7af6526c 100644 --- a/app/admin/model/system/sysConfig.go +++ b/app/admin/model/system/sysConfig.go @@ -5,7 +5,6 @@ import ( "ruoyi-go/pkg/mysql" "ruoyi-go/utils" "ruoyi-go/utils/R" - "strconv" "time" ) @@ -140,15 +139,6 @@ func checkConfigKeyUnique(configKey string) int64 { return keyCount } -func SelectCaptchaEnabled() bool { - var configValue = SelectConfigByKey("sys.account.captchaEnabled") - boolValue, err := strconv.ParseBool(configValue) - if err != nil { - return true - } - return boolValue -} - func SelectConfigByKey(configKey string) string { var config SysConfig err := mysql.MysqlDb().Where("config_key = ?", configKey).First(&config).Error @@ -171,12 +161,7 @@ func DelConfig(configIds string) R.Result { var ids = utils.Split(configIds) for i := 0; i < len(ids); i++ { id := ids[i] - var config = GetConfigInfo(id) - configType := config.ConfigType - if "Y" == configType { - panic(R.ReturnFailMsg("内置参数" + config.ConfigKey + "不能删除 ")) - } - DelConfigById(config.ConfigId) + DelConfigById(id) } return R.ReturnSuccess("操作成功") } @@ -187,21 +172,3 @@ func DelConfigById(configId int) { panic(R.ReturnFailMsg(err.Error())) } } - -/* -加载缓存 -重复初始化 -*/ -func loadingConfigCache() { - //var param = tools.SearchTableDataParam{} - //SelectConfigList(param, false) - /*重新赋值进去*/ - -} - -func DelCacheConfig(refreshCache string) R.Result { - /*删除所有缓存*/ - /*重复初始化*/ - loadingConfigCache() - return R.ReturnSuccess("操作成功") -} diff --git a/app/admin/router/system/config.go b/app/admin/router/system/config.go index 1f186dce30ba021175e30e2bb9908d68a7713e80..2ea4da052e9687426507b17ce3fd3fac3e98abbb 100644 --- a/app/admin/router/system/config.go +++ b/app/admin/router/system/config.go @@ -21,7 +21,7 @@ func InitConfig(e *gin.Engine) { auth.POST("", system.SaveConfig) auth.PUT("", system.UploadConfig) auth.DELETE("/:configIds", system.DetectConfig) - auth.DELETE("/donws/:refreshCache", system.DeleteCacheConfig) + auth.DELETE("/refreshCache", system.RefreshCacheConfig) } } } diff --git a/app/admin/router/system/user.go b/app/admin/router/system/user.go index 68b81fcdf009da2ffde851877a9f3cf622da46a3..21a7b3a11d38b73fe8c19a7f5245d7466ba65fca 100644 --- a/app/admin/router/system/user.go +++ b/app/admin/router/system/user.go @@ -1,10 +1,9 @@ package system import ( + "github.com/gin-gonic/gin" "ruoyi-go/app/admin/api/system" "ruoyi-go/utils/jwt" - - "github.com/gin-gonic/gin" ) func InitUser(e *gin.Engine) { @@ -21,6 +20,11 @@ func InitUser(e *gin.Engine) { auth.GET("/user/", system.GetUserInfo) auth.POST("/user", system.SaveUser) auth.PUT("/user", system.UploadUser) + // 更新用户信息 验证防重复提交和限流中间件 + //auth + //.Use(utils.RepeatSubmitMiddleware(500)) + //.Use(utils.RateLimiterMiddleware(60, 20, utils.DEFAULT)) + //.PUT("/user", system.UploadUser) auth.DELETE("/user/:userIds", system.DeleteUserById) auth.PUT("/user/resetPwd", system.ResetPwd) auth.PUT("/user/changeStatus", system.ChangeUserStatus) diff --git a/config/app_config.go b/config/app_config.go index adefb1d336f38ff09e47e2778807f8eaa56af82c..7408d60df490e3f4c67c53b5631ea23887b403cb 100644 --- a/config/app_config.go +++ b/config/app_config.go @@ -15,14 +15,16 @@ var Redis *redis var Jwt *jwt var XxlJob *xxlJob var LogConfig *logConfig +var UserPassword *userPassword type conf struct { - Svc server `yaml:"server"` - DB database `yaml:"database"` - RedisConfig redis `yaml:"redis"` - Jwt jwt `yaml:"jwt"` - XxlJob xxlJob `yaml:"xxl-job"` - LogConfig logConfig `yaml:"log"` + Svc server `yaml:"server"` + DB database `yaml:"database"` + RedisConfig redis `yaml:"redis"` + Jwt jwt `yaml:"jwt"` + XxlJob xxlJob `yaml:"xxl-job"` + LogConfig logConfig `yaml:"log"` + UserPassword userPassword `yaml:"user-password"` } type server struct { @@ -78,6 +80,10 @@ type logConfig struct { FilePath string `yaml:"filePath"` Filtered []string `yaml:"filtered"` } +type userPassword struct { + MaxRetryCount int `yaml:"maxRetryCount"` + LockTime int `yaml:"lockTime"` +} func InitAppConfig(dataFile string) { // 解决相对路经下获取不了配置文件问题 @@ -107,4 +113,5 @@ func InitAppConfig(dataFile string) { Jwt = &c.Jwt XxlJob = &c.XxlJob LogConfig = &c.LogConfig + UserPassword = &c.UserPassword } diff --git a/config/config.yaml.example b/config/config.yaml.example index 3888c7120602843e000942d46bc7ce209dcc3e6d..ab935d5149bac95015914c345d531df1dad444ca 100644 --- a/config/config.yaml.example +++ b/config/config.yaml.example @@ -53,4 +53,9 @@ log: logMode: default # default mysql file es filePath: home/ruoyi/log # 日志本地的话存放地址 filterate: # 日志过滤的接口 - - '/reg' \ No newline at end of file + - '/reg' +user-password: + # 密码最大错误次数 + maxRetryCount: 5 + # 密码锁定时间(默认10分钟) + lockTime: 10 \ No newline at end of file diff --git a/pkg/cache/redisCache/redisCache.go b/pkg/cache/redisCache/redisCache.go index 19ee43a47eeb9b2bff86e807610341e957333682..aeb6e2ff7d601c6f59da4ef484bca37db0468f26 100644 --- a/pkg/cache/redisCache/redisCache.go +++ b/pkg/cache/redisCache/redisCache.go @@ -34,3 +34,7 @@ func (r redisCache) Del(key string) (string, error) { func (r redisCache) Clear() (string, error) { return "", nil } + +func (r redisCache) Execute(script string, keys []string, args ...interface{}) (interface{}, error) { + return redis.Client().Eval(context.TODO(), script, keys, args).Result() +} diff --git a/pkg/cache/redisStore.go b/pkg/cache/redisStore.go new file mode 100644 index 0000000000000000000000000000000000000000..c54c80d0e2a853e07c19a1cadd5f712b9d54f884 --- /dev/null +++ b/pkg/cache/redisStore.go @@ -0,0 +1,43 @@ +package cache + +import ( + "fmt" + "ruoyi-go/app/admin/model/constants" + "ruoyi-go/pkg/cache/redisCache" + "time" +) + +type RedisStore struct { +} + +// Set 实现设置captcha的方法 +func (r RedisStore) Set(id string, value string) error { + key := constants.CaptchaCodesKey + id + err := redisCache.NewRedisCache().Put(key, value, time.Minute*3) + return err +} + +// Get 实现获取captcha的方法 +func (r RedisStore) Get(id string, clear bool) string { + key := constants.CaptchaCodesKey + id + val, err := redisCache.NewRedisCache().Get(key) + if err != nil { + fmt.Println(err) + return "" + } + if clear { + //clear为true,验证通过,删除这个验证码 + _, err := redisCache.NewRedisCache().Del(key) + if err != nil { + fmt.Println(err) + return "" + } + } + return val +} + +// Verify 实现验证captcha的方法 +func (r RedisStore) Verify(id, answer string, clear bool) bool { + v := RedisStore{}.Get(id, clear) + return v == answer +} diff --git a/utils/captchaImageUtils.go b/utils/captchaImageUtils.go index 39195e49dd872e5a91576eebdd76e379663e9830..cc234c17fd32eea23e98a456a0344896da45703d 100644 --- a/utils/captchaImageUtils.go +++ b/utils/captchaImageUtils.go @@ -4,10 +4,12 @@ import ( "github.com/mojocn/base64Captcha" "image/color" "ruoyi-go/config" - "time" + "ruoyi-go/pkg/cache" ) -var result = base64Captcha.NewMemoryStore(20240, 3*time.Minute) +// var store = base64Captcha.NewMemoryStore(20240, 3*time.Minute) +// 使用redis缓存 +var store base64Captcha.Store = cache.RedisStore{} // CreateImageCaptcha 生产 验证码 func CreateImageCaptcha() (string, string, error) { @@ -24,13 +26,13 @@ func CreateImageCaptcha() (string, string, error) { panic("生成验证码的类型没有配置,请在yaml文件中配置完再次重试启动项目") } // 创建验证码并传入创建的类型的配置,以及存储的对象 - c := base64Captcha.NewCaptcha(driver, result) + c := base64Captcha.NewCaptcha(driver, store) return c.Generate() } // VerifyCaptcha 验证 验证码 func VerifyCaptcha(Uuid string, Code string) bool { - return result.Verify(Uuid, Code, true) + return store.Verify(Uuid, Code, true) } // 配置 算数 验证码 diff --git a/utils/rateLimiter.go b/utils/rateLimiter.go new file mode 100644 index 0000000000000000000000000000000000000000..3d404f4d8585332adc147d38c2bf71c7858b55a0 --- /dev/null +++ b/utils/rateLimiter.go @@ -0,0 +1,61 @@ +package utils + +import ( + "github.com/gin-gonic/gin" + "log" + "net/http" + "ruoyi-go/app/admin/model/constants" + "ruoyi-go/pkg/cache/redisCache" + "ruoyi-go/utils/R" + "strings" +) + +// 定义Lua限流脚本 +const script = ` + local key = KEYS[1] + local count = tonumber(ARGV[1]) + local time = tonumber(ARGV[2]) + local current = redis.call('get', key) + if current and tonumber(current) > count then + return tonumber(current) + end + current = redis.call('incr', key) + if tonumber(current) == 1 then + redis.call('expire', key, time) + end + return tonumber(current) + ` + +type LimitType int + +const ( + DEFAULT LimitType = iota //限流类型 默认 + Ip //根据ip进行限流 +) + +// RateLimiterMiddleware 限流中间件 每 intervalS 秒 只能请求count次 +func RateLimiterMiddleware(intervalS int64, count int64, limitType LimitType) func(ctx *gin.Context) { + return func(ctx *gin.Context) { + combineKey := getCombineKey(limitType, ctx) + number, err := redisCache.NewRedisCache().Execute(script, []string{combineKey}, count, intervalS) + if err != nil { + return + } + if number == nil || number.(int64) > count { + ctx.JSON(http.StatusOK, R.ReturnFailMsg("访问过于频繁,请稍候再试")) + ctx.Abort() + return + } + log.Printf("限制请求:%v,当前请求:%v,缓存key:%v\n", count, number, combineKey) + } +} + +func getCombineKey(limitType LimitType, ctx *gin.Context) string { + requestURL := ctx.Request.URL.String() + method := ctx.Request.Method + key := constants.RateLimitCacheKey + strings.ReplaceAll(requestURL[1:], "/", "-") + method + if limitType == Ip { + key = key + ctx.ClientIP() + } + return key +} diff --git a/utils/repeatSubmit.go b/utils/repeatSubmit.go new file mode 100644 index 0000000000000000000000000000000000000000..aef506a1c46828e7f3207b4dccaab7b8fe46d021 --- /dev/null +++ b/utils/repeatSubmit.go @@ -0,0 +1,69 @@ +package utils + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "github.com/gin-gonic/gin" + "io" + "net/http" + "ruoyi-go/app/admin/model/constants" + "ruoyi-go/pkg/cache/redisCache" + "ruoyi-go/utils/R" + "ruoyi-go/utils/jwt" + "strings" + "time" +) + +type RequestInfo struct { + RepeatParams string `json:"repeatParams"` + RepeatTime int64 `json:"repeatTime"` +} + +// RepeatSubmitMiddleware 防重复提交组件 interval 单位 毫秒 +func RepeatSubmitMiddleware(intervalMs int64) func(ctx *gin.Context) { + return func(ctx *gin.Context) { + uuid, err := jwt.GetJwtUuid(ctx) + if len(uuid) <= 0 || err != nil { + return + } + bodyBytes, _ := io.ReadAll(ctx.Request.Body) + param := string(bodyBytes) + ctx.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + if bodyBytes == nil { + param = ctx.Request.URL.Query().Encode() + } + requestURL := ctx.Request.URL.String() + hash := sha256.New() + hash.Write([]byte(param)) + hashString := hex.EncodeToString(hash.Sum(nil)) + nowDataMap := RequestInfo{ + RepeatParams: hashString, + RepeatTime: time.Now().UnixNano() / int64(time.Millisecond), + } + + key := constants.RepeatSubmitCacheKey + strings.ReplaceAll(requestURL[1:], "/", "-") + "-" + uuid + get, err := redisCache.NewRedisCache().Get(key) + if len(get) > 0 { + sessionMap := make(map[string]RequestInfo) + err := json.Unmarshal([]byte(get), &sessionMap) + if err == nil { + if oldDataMap, exists := sessionMap[requestURL]; exists { + if oldDataMap.RepeatParams == nowDataMap.RepeatParams && (nowDataMap.RepeatTime-oldDataMap.RepeatTime <= intervalMs) { + ctx.JSON(http.StatusOK, R.ReturnFailMsg("不允许重复提交,请稍候再试")) + ctx.Abort() + return + } + } + } + } + + cacheMap := map[string]RequestInfo{ + requestURL: nowDataMap, + } + jsonData, err := json.Marshal(cacheMap) + redisCache.NewRedisCache().Put(key, string(jsonData), time.Duration(intervalMs)*time.Millisecond) + + } +}