From 36f43be90cfba9e86c882a70bd7323f10bd3156e Mon Sep 17 00:00:00 2001 From: RuoYi Date: Sat, 30 Jul 2022 14:01:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=85=8D=E7=BD=AE=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E6=9C=80=E5=A4=A7=E9=94=99=E8=AF=AF=E6=AC=A1=E6=95=B0?= =?UTF-8?q?/=E9=94=81=E5=AE=9A=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ruoyi/common/constant/CacheConstants.java | 5 + ...UserPasswordRetryLimitExceedException.java | 16 ++++ .../com/ruoyi/framework/redis/RedisCache.java | 22 +++++ .../context/AuthenticationContextHolder.java | 28 ++++++ .../security/service/SysLoginService.java | 6 +- .../security/service/SysPasswordService.java | 94 +++++++++++++++++++ .../service/UserDetailsServiceImpl.java | 8 +- .../framework/web/domain/AjaxResult.java | 21 +++++ .../monitor/controller/CacheController.java | 1 + src/main/resources/application.yml | 8 ++ src/main/resources/i18n/messages.properties | 2 +- 11 files changed, 206 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java create mode 100644 src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java create mode 100644 src/main/java/com/ruoyi/framework/security/service/SysPasswordService.java diff --git a/src/main/java/com/ruoyi/common/constant/CacheConstants.java b/src/main/java/com/ruoyi/common/constant/CacheConstants.java index 7ea15aa..c89692c 100644 --- a/src/main/java/com/ruoyi/common/constant/CacheConstants.java +++ b/src/main/java/com/ruoyi/common/constant/CacheConstants.java @@ -36,4 +36,9 @@ public class CacheConstants * 限流 redis key */ public static final String RATE_LIMIT_KEY = "rate_limit:"; + + /** + * 登录账户密码错误次数 redis key + */ + public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:"; } diff --git a/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java b/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java new file mode 100644 index 0000000..0de8d24 --- /dev/null +++ b/src/main/java/com/ruoyi/common/exception/user/UserPasswordRetryLimitExceedException.java @@ -0,0 +1,16 @@ +package com.ruoyi.common.exception.user; + +/** + * 用户错误最大次数异常类 + * + * @author ruoyi + */ +public class UserPasswordRetryLimitExceedException extends UserException +{ + private static final long serialVersionUID = 1L; + + public UserPasswordRetryLimitExceedException(int retryLimitCount, int lockTime) + { + super("user.password.retry.limit.exceed", new Object[] { retryLimitCount, lockTime }); + } +} diff --git a/src/main/java/com/ruoyi/framework/redis/RedisCache.java b/src/main/java/com/ruoyi/framework/redis/RedisCache.java index e701bcc..1e674db 100644 --- a/src/main/java/com/ruoyi/framework/redis/RedisCache.java +++ b/src/main/java/com/ruoyi/framework/redis/RedisCache.java @@ -61,6 +61,28 @@ public class RedisCache return expire(key, timeout, TimeUnit.SECONDS); } + /** + * 获取有效时间 + * + * @param key Redis键 + * @return 有效时间 + */ + public long getExpire(final String key) + { + return redisTemplate.getExpire(key); + } + + /** + * 判断 key是否存在 + * + * @param key 键 + * @return true 存在 false不存在 + */ + public Boolean hasKey(String key) + { + return redisTemplate.hasKey(key); + } + /** * 设置有效时间 * diff --git a/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java b/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java new file mode 100644 index 0000000..5ee5bbd --- /dev/null +++ b/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java @@ -0,0 +1,28 @@ +package com.ruoyi.framework.security.context; + +import org.springframework.security.core.Authentication; + +/** + * 身份验证信息 + * + * @author ruoyi + */ +public class AuthenticationContextHolder +{ + private static final ThreadLocal contextHolder = new ThreadLocal<>(); + + public static Authentication getContext() + { + return contextHolder.get(); + } + + public static void setContext(Authentication context) + { + contextHolder.set(context); + } + + public static void clearContext() + { + contextHolder.remove(); + } +} diff --git a/src/main/java/com/ruoyi/framework/security/service/SysLoginService.java b/src/main/java/com/ruoyi/framework/security/service/SysLoginService.java index f7c195c..3261d3b 100644 --- a/src/main/java/com/ruoyi/framework/security/service/SysLoginService.java +++ b/src/main/java/com/ruoyi/framework/security/service/SysLoginService.java @@ -22,6 +22,7 @@ import com.ruoyi.framework.manager.AsyncManager; import com.ruoyi.framework.manager.factory.AsyncFactory; import com.ruoyi.framework.redis.RedisCache; import com.ruoyi.framework.security.LoginUser; +import com.ruoyi.framework.security.context.AuthenticationContextHolder; import com.ruoyi.project.system.domain.SysUser; import com.ruoyi.project.system.service.ISysConfigService; import com.ruoyi.project.system.service.ISysUserService; @@ -70,9 +71,10 @@ public class SysLoginService Authentication authentication = null; try { + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); + AuthenticationContextHolder.setContext(authenticationToken); // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername - authentication = authenticationManager - .authenticate(new UsernamePasswordAuthenticationToken(username, password)); + authentication = authenticationManager.authenticate(authenticationToken); } catch (Exception e) { diff --git a/src/main/java/com/ruoyi/framework/security/service/SysPasswordService.java b/src/main/java/com/ruoyi/framework/security/service/SysPasswordService.java new file mode 100644 index 0000000..c50a073 --- /dev/null +++ b/src/main/java/com/ruoyi/framework/security/service/SysPasswordService.java @@ -0,0 +1,94 @@ +package com.ruoyi.framework.security.service; + +import java.util.concurrent.TimeUnit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import com.ruoyi.common.constant.CacheConstants; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.exception.user.UserPasswordNotMatchException; +import com.ruoyi.common.exception.user.UserPasswordRetryLimitExceedException; +import com.ruoyi.common.utils.MessageUtils; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.framework.manager.AsyncManager; +import com.ruoyi.framework.manager.factory.AsyncFactory; +import com.ruoyi.framework.redis.RedisCache; +import com.ruoyi.framework.security.context.AuthenticationContextHolder; +import com.ruoyi.project.system.domain.SysUser; + +/** + * 登录密码方法 + * + * @author ruoyi + */ +@Component +public class SysPasswordService +{ + @Autowired + private RedisCache redisCache; + + @Value(value = "${user.password.maxRetryCount}") + private int maxRetryCount; + + @Value(value = "${user.password.lockTime}") + private int lockTime; + + /** + * 登录账户密码错误次数缓存键名 + * + * @param username 用户名 + * @return 缓存键key + */ + private String getCacheKey(String username) + { + return CacheConstants.PWD_ERR_CNT_KEY + username; + } + + public void validate(SysUser user) + { + Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext(); + String username = usernamePasswordAuthenticationToken.getName(); + String password = usernamePasswordAuthenticationToken.getCredentials().toString(); + + Integer retryCount = redisCache.getCacheObject(getCacheKey(username)); + + if (retryCount == null) + { + retryCount = 0; + } + + if (retryCount >= Integer.valueOf(maxRetryCount).intValue()) + { + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, + MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount, lockTime))); + throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime); + } + + if (!matches(user, password)) + { + retryCount = retryCount + 1; + AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, + MessageUtils.message("user.password.retry.limit.count", retryCount))); + redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES); + throw new UserPasswordNotMatchException(); + } + else + { + clearLoginRecordCache(username); + } + } + + public boolean matches(SysUser user, String rawPassword) + { + return SecurityUtils.matchesPassword(rawPassword, user.getPassword()); + } + + public void clearLoginRecordCache(String loginName) + { + if (redisCache.hasKey(getCacheKey(loginName))) + { + redisCache.deleteObject(getCacheKey(loginName)); + } + } +} diff --git a/src/main/java/com/ruoyi/framework/security/service/UserDetailsServiceImpl.java b/src/main/java/com/ruoyi/framework/security/service/UserDetailsServiceImpl.java index 4b4a2ff..d48c4c9 100644 --- a/src/main/java/com/ruoyi/framework/security/service/UserDetailsServiceImpl.java +++ b/src/main/java/com/ruoyi/framework/security/service/UserDetailsServiceImpl.java @@ -5,7 +5,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import com.ruoyi.common.enums.UserStatus; import com.ruoyi.common.exception.ServiceException; @@ -26,12 +25,15 @@ public class UserDetailsServiceImpl implements UserDetailsService @Autowired private ISysUserService userService; + + @Autowired + private SysPasswordService passwordService; @Autowired private SysPermissionService permissionService; @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException + public UserDetails loadUserByUsername(String username) { SysUser user = userService.selectUserByUserName(username); if (StringUtils.isNull(user)) @@ -50,6 +52,8 @@ public class UserDetailsServiceImpl implements UserDetailsService throw new ServiceException("对不起,您的账号:" + username + " 已停用"); } + passwordService.validate(user); + return createLoginUser(user); } diff --git a/src/main/java/com/ruoyi/framework/web/domain/AjaxResult.java b/src/main/java/com/ruoyi/framework/web/domain/AjaxResult.java index 51253c6..cba99ba 100644 --- a/src/main/java/com/ruoyi/framework/web/domain/AjaxResult.java +++ b/src/main/java/com/ruoyi/framework/web/domain/AjaxResult.java @@ -1,6 +1,7 @@ package com.ruoyi.framework.web.domain; import java.util.HashMap; +import java.util.Objects; import com.ruoyi.common.constant.HttpStatus; import com.ruoyi.common.utils.StringUtils; @@ -146,6 +147,26 @@ public class AjaxResult extends HashMap return new AjaxResult(code, msg, null); } + /** + * 是否为成功消息 + * + * @return 结果 + */ + public boolean isSuccess() + { + return !isError(); + } + + /** + * 是否为错误消息 + * + * @return 结果 + */ + public boolean isError() + { + return Objects.equals(HttpStatus.ERROR, this.get(CODE_TAG)); + } + /** * 方便链式调用 * diff --git a/src/main/java/com/ruoyi/project/monitor/controller/CacheController.java b/src/main/java/com/ruoyi/project/monitor/controller/CacheController.java index 37bf080..8dce0e6 100644 --- a/src/main/java/com/ruoyi/project/monitor/controller/CacheController.java +++ b/src/main/java/com/ruoyi/project/monitor/controller/CacheController.java @@ -41,6 +41,7 @@ public class CacheController caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码")); caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交")); caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理")); + caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数")); } @PreAuthorize("@ss.hasPermi('monitor:cache:list')") diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9fefd41..fd6dc2c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -39,6 +39,14 @@ logging: com.ruoyi: debug org.springframework: warn +# 用户配置 +user: + password: + # 密码最大错误次数 + maxRetryCount: 5 + # 密码锁定时间(默认10分钟) + lockTime: 10 + # Spring配置 spring: # 资源信息 diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 5a41f8c..b7433dc 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -5,7 +5,7 @@ user.jcaptcha.expire=验证码已失效 user.not.exists=用户不存在/密码错误 user.password.not.match=用户不存在/密码错误 user.password.retry.limit.count=密码输入错误{0}次 -user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定10分钟 +user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟 user.password.delete=对不起,您的账号已被删除 user.blocked=用户已封禁,请联系管理员 role.blocked=角色已封禁,请联系管理员