From 3b2f9898de4e5f52db522a9fd9915f60c9a1a8c6 Mon Sep 17 00:00:00 2001 From: zhuguangxin Date: Sat, 5 Oct 2024 16:03:49 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=E5=AE=8C=E5=96=84=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/api/system/config.go | 53 ++++++++++++++++---- app/admin/api/system/user.go | 4 +- app/admin/model/cache/cache.go | 3 ++ app/admin/model/cache/sysConfigCache.go | 56 ++++++++++++++++++++++ app/admin/model/constants/sys_constants.go | 9 ++-- app/admin/model/system/sysConfig.go | 35 +------------- app/admin/router/system/config.go | 2 +- 7 files changed, 112 insertions(+), 50 deletions(-) create mode 100644 app/admin/model/cache/sysConfigCache.go diff --git a/app/admin/api/system/config.go b/app/admin/api/system/config.go index be2191a..10c83bc 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 aa6819a..45a8bb6 100644 --- a/app/admin/api/system/user.go +++ b/app/admin/api/system/user.go @@ -43,7 +43,7 @@ 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 } @@ -151,7 +151,7 @@ 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 { diff --git a/app/admin/model/cache/cache.go b/app/admin/model/cache/cache.go index cb388d7..18bd3bd 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/sysConfigCache.go b/app/admin/model/cache/sysConfigCache.go new file mode 100644 index 0000000..9c907dc --- /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 60d0ed4..9aad2be 100644 --- a/app/admin/model/constants/sys_constants.go +++ b/app/admin/model/constants/sys_constants.go @@ -16,8 +16,9 @@ const ( ) const ( - LoginCacheKey = "login_tokens:" - CaptchaCodesKey = "captcha_codes:" - SysDictCacheKey = "sys_dict:" - ScanCountMax = 1000 + LoginCacheKey = "login_tokens:" + CaptchaCodesKey = "captcha_codes:" + SysDictCacheKey = "sys_dict:" + SysConfigCacheKey = "sys_config:" + ScanCountMax = 1000 ) diff --git a/app/admin/model/system/sysConfig.go b/app/admin/model/system/sysConfig.go index 31fe131..281d845 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 1f186dc..2ea4da0 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) } } } -- Gitee From f757c8515693dd13edd758e515ecbd2cb3107af4 Mon Sep 17 00:00:00 2001 From: zhuguangxin Date: Sat, 5 Oct 2024 16:31:02 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=E5=AE=8C=E5=96=84=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E7=A0=81=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/api/system/user.go | 2 +- pkg/cache/redisStore.go | 43 ++++++++++++++++++++++++++++++++++++ utils/captchaImageUtils.go | 10 +++++---- 3 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 pkg/cache/redisStore.go diff --git a/app/admin/api/system/user.go b/app/admin/api/system/user.go index 45a8bb6..669d834 100644 --- a/app/admin/api/system/user.go +++ b/app/admin/api/system/user.go @@ -155,7 +155,7 @@ func CaptchaImageHandler(context *gin.Context) { 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/pkg/cache/redisStore.go b/pkg/cache/redisStore.go new file mode 100644 index 0000000..c54c80d --- /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 39195e4..cc234c1 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) } // 配置 算数 验证码 -- Gitee From 95cdc276f4c38632ce64768a977db07d221f2c00 Mon Sep 17 00:00:00 2001 From: zhuguangxin Date: Sun, 6 Oct 2024 00:11:05 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=E7=99=BB=E5=BD=95=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E6=8F=90=E4=BA=A4=E7=BC=93=E5=AD=98=E5=AE=8C?= =?UTF-8?q?=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/api/system/user.go | 100 ++++++++++++--------- app/admin/model/cache/pwdErrCntCache.go | 32 +++++++ app/admin/model/constants/sys_constants.go | 1 + config/app_config.go | 19 ++-- config/config.yaml.example | 7 +- 5 files changed, 108 insertions(+), 51 deletions(-) create mode 100644 app/admin/model/cache/pwdErrCntCache.go diff --git a/app/admin/api/system/user.go b/app/admin/api/system/user.go index 669d834..d9d25e0 100644 --- a/app/admin/api/system/user.go +++ b/app/admin/api/system/user.go @@ -6,6 +6,7 @@ import ( "github.com/google/uuid" "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" @@ -43,6 +44,7 @@ func LoginHandler(context *gin.Context) { findUser(param, context) } func loginBeforeCheck(param system.LoginParam, context *gin.Context) bool { + // 验证码验证 var captchaEnabled = SelectCaptchaEnabled() if !captchaEnabled { return false @@ -63,56 +65,66 @@ 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 - } - //写入缓存 - loginUser, _ := json.Marshal(&monitor.LoginUserCache{ - UserId: user.UserId, - UserName: user.UserName, - Uuid: newUUID.String(), - 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, - }) - } 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() + 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{ + UserId: user.UserId, + UserName: user.UserName, + Uuid: newUUID.String(), + 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) { diff --git a/app/admin/model/cache/pwdErrCntCache.go b/app/admin/model/cache/pwdErrCntCache.go new file mode 100644 index 0000000..9ad544a --- /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.PwdErrCnt + 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.PwdErrCnt+username, strconv.Itoa(count), time.Duration(config.UserPassword.LockTime)*time.Minute) +} + +// DeletePasswordTryCount 删除 +func DeletePasswordTryCount(username string) { + redisCache.NewRedisCache().Del(constants.PwdErrCnt + username) +} diff --git a/app/admin/model/constants/sys_constants.go b/app/admin/model/constants/sys_constants.go index 9aad2be..83086fb 100644 --- a/app/admin/model/constants/sys_constants.go +++ b/app/admin/model/constants/sys_constants.go @@ -20,5 +20,6 @@ const ( CaptchaCodesKey = "captcha_codes:" SysDictCacheKey = "sys_dict:" SysConfigCacheKey = "sys_config:" + PwdErrCnt = "pwd_err_cnt:" ScanCountMax = 1000 ) diff --git a/config/app_config.go b/config/app_config.go index adefb1d..7408d60 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 3888c71..ab935d5 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 -- Gitee From 79a6184c44446322731013539b85a75d03199716 Mon Sep 17 00:00:00 2001 From: zhuguangxin Date: Sun, 6 Oct 2024 15:36:31 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=E9=98=B2=E9=87=8D=E5=A4=8D=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/model/cache/pwdErrCntCache.go | 6 +- app/admin/model/constants/sys_constants.go | 13 ++-- app/admin/router/system/user.go | 6 +- utils/repeatSubmit.go | 69 ++++++++++++++++++++++ 4 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 utils/repeatSubmit.go diff --git a/app/admin/model/cache/pwdErrCntCache.go b/app/admin/model/cache/pwdErrCntCache.go index 9ad544a..830a1c4 100644 --- a/app/admin/model/cache/pwdErrCntCache.go +++ b/app/admin/model/cache/pwdErrCntCache.go @@ -10,7 +10,7 @@ import ( // GetPasswordTryCount 获取输入次数 func GetPasswordTryCount(username string) int { - countStr, err := redisCache.NewRedisCache().Get(constants.PwdErrCnt + username) + countStr, err := redisCache.NewRedisCache().Get(constants.PwdErrCntCacheKey + username) if err != nil || len(countStr) <= 0 { return 0 } @@ -23,10 +23,10 @@ func GetPasswordTryCount(username string) int { // SetPasswordTryCount 次数+1 func SetPasswordTryCount(username string, count int) { - redisCache.NewRedisCache().Put(constants.PwdErrCnt+username, strconv.Itoa(count), time.Duration(config.UserPassword.LockTime)*time.Minute) + 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.PwdErrCnt + username) + redisCache.NewRedisCache().Del(constants.PwdErrCntCacheKey + username) } diff --git a/app/admin/model/constants/sys_constants.go b/app/admin/model/constants/sys_constants.go index 83086fb..5c3efa2 100644 --- a/app/admin/model/constants/sys_constants.go +++ b/app/admin/model/constants/sys_constants.go @@ -16,10 +16,11 @@ const ( ) const ( - LoginCacheKey = "login_tokens:" - CaptchaCodesKey = "captcha_codes:" - SysDictCacheKey = "sys_dict:" - SysConfigCacheKey = "sys_config:" - PwdErrCnt = "pwd_err_cnt:" - ScanCountMax = 1000 + LoginCacheKey = "login_tokens:" + CaptchaCodesKey = "captcha_codes:" + SysDictCacheKey = "sys_dict:" + SysConfigCacheKey = "sys_config:" + PwdErrCntCacheKey = "pwd_err_cnt:" + RepeatSubmitCacheKey = "repeat_submit:" + ScanCountMax = 1000 ) diff --git a/app/admin/router/system/user.go b/app/admin/router/system/user.go index 68b81fc..4fcd56e 100644 --- a/app/admin/router/system/user.go +++ b/app/admin/router/system/user.go @@ -1,10 +1,10 @@ package system import ( + "github.com/gin-gonic/gin" "ruoyi-go/app/admin/api/system" + "ruoyi-go/utils" "ruoyi-go/utils/jwt" - - "github.com/gin-gonic/gin" ) func InitUser(e *gin.Engine) { @@ -20,7 +20,7 @@ func InitUser(e *gin.Engine) { auth.GET("/user/:userId", system.GetUserInfo) auth.GET("/user/", system.GetUserInfo) auth.POST("/user", system.SaveUser) - auth.PUT("/user", system.UploadUser) + auth.Use(utils.RepeatSubmitMiddleware(500)).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/utils/repeatSubmit.go b/utils/repeatSubmit.go new file mode 100644 index 0000000..aef506a --- /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) + + } +} -- Gitee From 310672477420db88d630101b35e2ad10f17526a2 Mon Sep 17 00:00:00 2001 From: zhuguangxin Date: Sun, 6 Oct 2024 19:32:56 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=E5=AE=8C=E5=96=84=E9=99=90=E6=B5=81?= =?UTF-8?q?=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/model/constants/sys_constants.go | 1 + app/admin/router/system/user.go | 8 ++- pkg/cache/redisCache/redisCache.go | 4 ++ utils/rateLimiter.go | 61 ++++++++++++++++++++++ 4 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 utils/rateLimiter.go diff --git a/app/admin/model/constants/sys_constants.go b/app/admin/model/constants/sys_constants.go index 5c3efa2..f5ee925 100644 --- a/app/admin/model/constants/sys_constants.go +++ b/app/admin/model/constants/sys_constants.go @@ -22,5 +22,6 @@ const ( SysConfigCacheKey = "sys_config:" PwdErrCntCacheKey = "pwd_err_cnt:" RepeatSubmitCacheKey = "repeat_submit:" + RateLimitCacheKey = "rate_limit:" ScanCountMax = 1000 ) diff --git a/app/admin/router/system/user.go b/app/admin/router/system/user.go index 4fcd56e..21a7b3a 100644 --- a/app/admin/router/system/user.go +++ b/app/admin/router/system/user.go @@ -3,7 +3,6 @@ package system import ( "github.com/gin-gonic/gin" "ruoyi-go/app/admin/api/system" - "ruoyi-go/utils" "ruoyi-go/utils/jwt" ) @@ -20,7 +19,12 @@ func InitUser(e *gin.Engine) { auth.GET("/user/:userId", system.GetUserInfo) auth.GET("/user/", system.GetUserInfo) auth.POST("/user", system.SaveUser) - auth.Use(utils.RepeatSubmitMiddleware(500)).PUT("/user", system.UploadUser) + 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/pkg/cache/redisCache/redisCache.go b/pkg/cache/redisCache/redisCache.go index cbbc72c..22a5b15 100644 --- a/pkg/cache/redisCache/redisCache.go +++ b/pkg/cache/redisCache/redisCache.go @@ -33,3 +33,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/utils/rateLimiter.go b/utils/rateLimiter.go new file mode 100644 index 0000000..3d404f4 --- /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 +} -- Gitee